Return-Path: X-Original-To: apmail-brooklyn-commits-archive@minotaur.apache.org Delivered-To: apmail-brooklyn-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 9FB8E17B8C for ; Wed, 15 Apr 2015 05:52:02 +0000 (UTC) Received: (qmail 54859 invoked by uid 500); 15 Apr 2015 05:51:59 -0000 Delivered-To: apmail-brooklyn-commits-archive@brooklyn.apache.org Received: (qmail 54835 invoked by uid 500); 15 Apr 2015 05:51:59 -0000 Mailing-List: contact commits-help@brooklyn.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@brooklyn.incubator.apache.org Delivered-To: mailing list commits@brooklyn.incubator.apache.org Received: (qmail 54706 invoked by uid 99); 15 Apr 2015 05:51:59 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 15 Apr 2015 05:51:59 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED,T_FILL_THIS_FORM_SHORT,T_RP_MATCHES_RCVD X-Spam-Check-By: apache.org Received: from [140.211.11.3] (HELO mail.apache.org) (140.211.11.3) by apache.org (qpsmtpd/0.29) with SMTP; Wed, 15 Apr 2015 05:51:57 +0000 Received: (qmail 54261 invoked by uid 99); 15 Apr 2015 05:51:37 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 15 Apr 2015 05:51:37 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 3D947E040F; Wed, 15 Apr 2015 05:51:37 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: aledsage@apache.org To: commits@brooklyn.incubator.apache.org Date: Wed, 15 Apr 2015 05:51:37 -0000 Message-Id: <8405af6847a9449c861cb68ce1ba0313@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [1/2] incubator-brooklyn git commit: Adds CreateUserPolicy X-Virus-Checked: Checked by ClamAV on apache.org Repository: incubator-brooklyn Updated Branches: refs/heads/master 4f6fa6ca9 -> 10853fc04 Adds CreateUserPolicy Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/bea997bc Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/bea997bc Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/bea997bc Branch: refs/heads/master Commit: bea997bc762cc55d1c188245ccb98a53606db883 Parents: 4f6fa6c Author: Aled Sage Authored: Tue Apr 14 14:48:53 2015 -0500 Committer: Aled Sage Committed: Wed Apr 15 00:40:45 2015 -0500 ---------------------------------------------------------------------- .../brooklyn/policy/os/CreateUserPolicy.java | 174 +++++++++++++++++++ .../policy/os/CreateUserPolicyLiveTest.java | 113 ++++++++++++ .../policy/os/CreateUserPolicyTest.java | 138 +++++++++++++++ 3 files changed, 425 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bea997bc/locations/jclouds/src/main/java/brooklyn/policy/os/CreateUserPolicy.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/brooklyn/policy/os/CreateUserPolicy.java b/locations/jclouds/src/main/java/brooklyn/policy/os/CreateUserPolicy.java new file mode 100644 index 0000000..80851da --- /dev/null +++ b/locations/jclouds/src/main/java/brooklyn/policy/os/CreateUserPolicy.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package brooklyn.policy.os; + +import java.util.List; + +import org.jclouds.compute.config.AdminAccessConfiguration; +import org.jclouds.scriptbuilder.functions.InitAdminAccess; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.jclouds.scriptbuilder.statements.ssh.SshdConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.Entity; +import brooklyn.entity.basic.AbstractEntity; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.EntityInternal; +import brooklyn.entity.basic.EntityLocal; +import brooklyn.event.AttributeSensor; +import brooklyn.event.SensorEvent; +import brooklyn.event.SensorEventListener; +import brooklyn.event.basic.Sensors; +import brooklyn.location.Location; +import brooklyn.location.basic.SshMachineLocation; +import brooklyn.policy.basic.AbstractPolicy; +import brooklyn.util.flags.SetFromFlag; +import brooklyn.util.internal.ssh.SshTool; +import brooklyn.util.text.Identifiers; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * When attached to an entity, this will monitor for when an {@link SshMachineLocation} is added to that entity + * (e.g. when a VM has been provisioned for it). + * + * The policy will then (asynchronously) add a new user to the VM, with a randomly generated password. + * The ssh details will be set as a sensor on the entity. + * + * If this is used, it is strongly encouraged to tell users to change the password on first login. + * + * A preferred mechanism would be for an external key-management tool to generate ssh key-pairs for + * the user, and for the public key to be passed to Brooklyn. However, there is not a customer + * requirement for that yet, so focusing on the password-approach. + */ +@Beta +public class CreateUserPolicy extends AbstractPolicy implements SensorEventListener { + + // TODO Should add support for authorizing ssh keys as well + + // TODO Should review duplication with: + // - JcloudsLocationConfig.GRANT_USER_SUDO + // (but config default/description and context of use are different) + // - AdminAccess in JcloudsLocation.createUserStatements + + // TODO Could make the password explicitly configurable, or auto-generate if not set? + + private static final Logger LOG = LoggerFactory.getLogger(CreateUserPolicy.class); + + @SetFromFlag("user") + public static final ConfigKey VM_USERNAME = ConfigKeys.newStringConfigKey("createuser.vm.user.name"); + + @SetFromFlag("grantSudo") + public static final ConfigKey GRANT_SUDO = ConfigKeys.newBooleanConfigKey( + "createuser.vm.user.grantSudo", + "Whether to give the new user sudo rights", + false); + + public static final AttributeSensor VM_USER_CREDENTIALS = Sensors.newStringSensor( + "createuser.vm.user.credentials", + "The \" : @ :\""); + + public void setEntity(EntityLocal entity) { + super.setEntity(entity); + subscribe(entity, AbstractEntity.LOCATION_ADDED, this); + } + + @Override + public void onEvent(SensorEvent event) { + final Entity entity = event.getSource(); + final Location loc = event.getValue(); + if (loc instanceof SshMachineLocation) { + addUserAsync(entity, (SshMachineLocation)loc); + } + } + + protected void addUserAsync(final Entity entity, final SshMachineLocation machine) { + ((EntityInternal)entity).getExecutionContext().execute(new Runnable() { + public void run() { + addUser(entity, machine); + }}); + } + + protected void addUser(Entity entity, SshMachineLocation machine) { + boolean grantSudo = getRequiredConfig(GRANT_SUDO); + String user = getRequiredConfig(VM_USERNAME); + String password = Identifiers.makeRandomId(12); + String hostname = machine.getAddress().getHostName(); + int port = machine.getPort(); + String creds = user + " : " + password + " @ " +hostname + ":" + port; + + LOG.info("Adding auto-generated user "+user+" @ "+hostname+":"+port); + + // Build the command to create the user + // Note AdminAccess requires _all_ fields set, due to http://code.google.com/p/jclouds/issues/detail?id=1095 + // If jclouds grants Sudo rights, it overwrites the /etc/sudoers, which makes integration tests very dangerous! Not using it. + AdminAccess adminAccess = AdminAccess.builder() + .adminUsername(user) + .adminPassword(password) + .grantSudoToAdminUser(false) + .resetLoginPassword(true) + .loginPassword(password) + .authorizeAdminPublicKey(false) + .adminPublicKey("ignored") + .installAdminPrivateKey(false) + .adminPrivateKey("ignore") + .lockSsh(false) + .build(); + + org.jclouds.scriptbuilder.domain.OsFamily scriptOsFamily = (machine.getMachineDetails().getOsDetails().isWindows()) + ? org.jclouds.scriptbuilder.domain.OsFamily.WINDOWS + : org.jclouds.scriptbuilder.domain.OsFamily.UNIX; + + InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default()); + initAdminAccess.visit(adminAccess); + String cmd = adminAccess.render(scriptOsFamily); + + // Exec command to create the user + int result = machine.execScript(ImmutableMap.of(SshTool.PROP_RUN_AS_ROOT.getName(), true), "create-user-"+user, ImmutableList.of(cmd)); + if (result != 0) { + throw new IllegalStateException("Failed to auto-generate user, using command "+cmd); + } + + // Exec command to grant password-access to sshd (which may have been disabled earlier). + cmd = new SshdConfig(ImmutableMap.of("PasswordAuthentication", "yes")).render(scriptOsFamily); + result = machine.execScript(ImmutableMap.of(SshTool.PROP_RUN_AS_ROOT.getName(), true), "create-user-"+user, ImmutableList.of(cmd)); + if (result != 0) { + throw new IllegalStateException("Failed to enable ssh-login-with-password, using command "+cmd); + } + + // Exec command to grant sudo rights. + if (grantSudo) { + List cmds = ImmutableList.of( + "cat >> /etc/sudoers <<-'END_OF_JCLOUDS_FILE'\n"+ + user+" ALL = (ALL) NOPASSWD:ALL\n"+ + "END_OF_JCLOUDS_FILE\n", + "chmod 0440 /etc/sudoers"); + result = machine.execScript(ImmutableMap.of(SshTool.PROP_RUN_AS_ROOT.getName(), true), "add-user-to-sudoers-"+user, cmds); + if (result != 0) { + throw new IllegalStateException("Failed to auto-generate user, using command "+cmd); + } + } + + ((EntityLocal)entity).setAttribute(VM_USER_CREDENTIALS, creds); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bea997bc/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyLiveTest.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyLiveTest.java b/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyLiveTest.java new file mode 100644 index 0000000..2913096 --- /dev/null +++ b/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyLiveTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package brooklyn.policy.os; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import brooklyn.entity.BrooklynAppLiveTestSupport; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.location.Location; +import brooklyn.location.LocationSpec; +import brooklyn.location.MachineProvisioningLocation; +import brooklyn.location.basic.LocalhostMachineProvisioningLocation; +import brooklyn.location.basic.SshMachineLocation; +import brooklyn.policy.PolicySpec; +import brooklyn.policy.os.CreateUserPolicy; +import brooklyn.test.EntityTestUtils; +import brooklyn.test.entity.TestEntity; +import brooklyn.util.internal.ssh.SshTool; +import brooklyn.util.text.Identifiers; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +public class CreateUserPolicyLiveTest extends BrooklynAppLiveTestSupport { + + private static final Logger LOG = LoggerFactory.getLogger(CreateUserPolicyLiveTest.class); + + // TODO Fails on OS X + @Test(groups={"Integration", "WIP"}) + public void testIntegrationCreatesUser() throws Exception { + LocalhostMachineProvisioningLocation loc = app.newLocalhostProvisioningLocation(); + runTestCreatesUser(loc); + } + + @Test(groups="Live") + @SuppressWarnings("unchecked") + public void testLiveCreatesUser() throws Exception { + String locSpec = "jclouds:softlayer:ams01"; + ImmutableMap locArgs = ImmutableMap.of("imageId", "CENTOS_6_64"); + Location loc = mgmt.getLocationRegistry().resolve(locSpec, locArgs); + runTestCreatesUser((MachineProvisioningLocation) loc); + } + + public void runTestCreatesUser(MachineProvisioningLocation loc) throws Exception { + String newUsername = Identifiers.makeRandomId(16); + SshMachineLocation machine = loc.obtain(ImmutableMap.of()); + + try { + app.createAndManageChild(EntitySpec.create(TestEntity.class) + .policy(PolicySpec.create(CreateUserPolicy.class) + .configure(CreateUserPolicy.GRANT_SUDO, true) + .configure(CreateUserPolicy.VM_USERNAME, newUsername))); + TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren()); + + app.start(ImmutableList.of(machine)); + + String creds = EntityTestUtils.assertAttributeEventuallyNonNull(entity, CreateUserPolicy.VM_USER_CREDENTIALS); + Pattern pattern = Pattern.compile("(.*) : (.*) @ (.*):(.*)"); + Matcher matcher = pattern.matcher(creds); + assertTrue(matcher.matches()); + String username = matcher.group(1).trim(); + String password = matcher.group(2).trim(); + String hostname = matcher.group(3).trim(); + String port = matcher.group(4).trim(); + + assertEquals(newUsername, username); + assertEquals(hostname, machine.getAddress().getHostName()); + assertEquals(port, ""+machine.getPort()); + + SshMachineLocation machine2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) + .configure(SshTool.PROP_USER, newUsername) + .configure(SshMachineLocation.PASSWORD, password) + .configure("address", hostname) + .configure(SshMachineLocation.SSH_PORT, Integer.parseInt(port))); + + LOG.info("Checking ssh'able for auto-generated user: machine="+machine+"; creds="+creds); + assertTrue(machine2.isSshable(), "machine="+machine+"; creds="+creds); + + } catch (Exception e) { + throw e; + } finally { + LOG.info("Deleting auto-generated user "+newUsername); + machine.execScript("delete-user-"+newUsername, ImmutableList.of("sudo userdel -f "+newUsername)); + + loc.release(machine); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bea997bc/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyTest.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyTest.java b/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyTest.java new file mode 100644 index 0000000..855d994 --- /dev/null +++ b/locations/jclouds/src/test/java/brooklyn/policy/os/CreateUserPolicyTest.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package brooklyn.policy.os; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import brooklyn.entity.BrooklynAppUnitTestSupport; +import brooklyn.entity.proxying.EntitySpec; +import brooklyn.location.LocationSpec; +import brooklyn.location.basic.SshMachineLocation; +import brooklyn.policy.PolicySpec; +import brooklyn.policy.os.CreateUserPolicy; +import brooklyn.test.EntityTestUtils; +import brooklyn.test.entity.TestEntity; +import brooklyn.util.internal.ssh.SshTool; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class CreateUserPolicyTest extends BrooklynAppUnitTestSupport { + + @SuppressWarnings("unused") + private static final Logger LOG = LoggerFactory.getLogger(CreateUserPolicyTest.class); + + public static class RecordingSshMachineLocation extends SshMachineLocation { + private static final long serialVersionUID = 1641930081769106380L; + + public static List> execScriptCalls = Lists.newArrayList(); + + @Override + public int execScript(String summary, List cmds) { + execScriptCalls.add(cmds); + return 0; + } + @Override + public int execScript(Map props, String summaryForLogging, List cmds) { + execScriptCalls.add(cmds); + return 0; + } + @Override + public int execScript(String summaryForLogging, List cmds, Map env) { + execScriptCalls.add(cmds); + return 0; + } + @Override + public int execScript(Map props, String summaryForLogging, List cmds, Map env) { + execScriptCalls.add(cmds); + return 0; + } + } + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + RecordingSshMachineLocation.execScriptCalls.clear(); + } + + @AfterMethod(alwaysRun=true) + @Override + public void tearDown() throws Exception { + try { + super.tearDown(); + } finally { + RecordingSshMachineLocation.execScriptCalls.clear(); + } + } + + @Test + public void testCallsCreateUser() throws Exception { + SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(RecordingSshMachineLocation.class) + .configure(SshTool.PROP_USER, "myuser") + .configure(SshTool.PROP_PASSWORD, "mypassword") + .configure("address", "1.2.3.4") + .configure(SshTool.PROP_PORT, 1234)); + + String newUsername = "mynewuser"; + + app.createAndManageChild(EntitySpec.create(TestEntity.class) + .policy(PolicySpec.create(CreateUserPolicy.class) + .configure(CreateUserPolicy.GRANT_SUDO, true) + .configure(CreateUserPolicy.VM_USERNAME, newUsername))); + TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren()); + app.start(ImmutableList.of(machine)); + + String creds = EntityTestUtils.assertAttributeEventuallyNonNull(entity, CreateUserPolicy.VM_USER_CREDENTIALS); + Pattern pattern = Pattern.compile("(.*) : (.*) @ (.*):(.*)"); + Matcher matcher = pattern.matcher(creds); + assertTrue(matcher.matches()); + String username2 = matcher.group(1).trim(); + String password = matcher.group(2).trim(); + String hostname = matcher.group(3).trim(); + String port = matcher.group(4).trim(); + + assertEquals(newUsername, username2); + assertEquals(hostname, "1.2.3.4"); + assertEquals(password.length(), 12); + assertEquals(port, "1234"); + + boolean found = false; + for (List cmds : RecordingSshMachineLocation.execScriptCalls) { + if (cmds.toString().contains("useradd")) { + found = true; + break; + } + } + assertTrue(found, "useradd not found in: "+RecordingSshMachineLocation.execScriptCalls); + } +}