/* * 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 org.apache.brooklyn.policy.jclouds.os; import java.util.List; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.policy.AbstractPolicy; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.location.ssh.SshMachineLocation; import org.apache.brooklyn.util.core.flags.SetFromFlag; import org.apache.brooklyn.util.core.internal.ssh.SshTool; import static org.apache.brooklyn.util.ssh.BashCommands.sbinPath; import org.apache.brooklyn.util.text.Identifiers; 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 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<Location> { // 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<String> VM_USERNAME = ConfigKeys.newStringConfigKey("createuser.vm.user.name"); @SetFromFlag("grantSudo") public static final ConfigKey<Boolean> GRANT_SUDO = ConfigKeys.newBooleanConfigKey( "createuser.vm.user.grantSudo", "Whether to give the new user sudo rights", false); public static final AttributeSensor<String> VM_USER_CREDENTIALS = Sensors.newStringSensor( "createuser.vm.user.credentials", "The \"<user> : <password> @ <hostname>:<port>\""); @SetFromFlag("resetLoginUser") public static final ConfigKey<Boolean> RESET_LOGIN_USER = ConfigKeys.newBooleanConfigKey( "createuser.vm.user.resetLoginUser", "Whether to reset the password used for user login", false); public void setEntity(EntityLocal entity) { super.setEntity(entity); subscriptions().subscribe(entity, AbstractEntity.LOCATION_ADDED, this); } @Override public void onEvent(SensorEvent<Location> 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); boolean resetPassword = getRequiredConfig(RESET_LOGIN_USER); 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(resetPassword) .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), ImmutableMap.of("PATH", sbinPath())); 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), ImmutableMap.of("PATH", sbinPath())); 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<String> 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, ImmutableMap.of("PATH", sbinPath())); if (result != 0) { throw new IllegalStateException("Failed to auto-generate user, using command "+cmds); } } ((EntityLocal)entity).sensors().set(VM_USER_CREDENTIALS, creds); } }