Repository: incubator-brooklyn Updated Branches: refs/heads/master 681b8fe90 -> e668d15e2
Windows support improvements in JcloudsLocation - improved support for windows credentials and for sshHostAndPortOverride Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/ae3790ee Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/ae3790ee Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/ae3790ee Branch: refs/heads/master Commit: ae3790eef6a16625038cf15be481e715e32a5151 Parents: f7b9047 Author: Aled Sage <[email protected]> Authored: Fri Jun 5 10:38:43 2015 +0100 Committer: Aled Sage <[email protected]> Committed: Wed Jun 10 21:48:28 2015 +0100 ---------------------------------------------------------------------- .../location/basic/WinRmMachineLocation.java | 9 + .../location/jclouds/JcloudsLocation.java | 217 ++++++++++++------- 2 files changed, 150 insertions(+), 76 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ae3790ee/core/src/main/java/brooklyn/location/basic/WinRmMachineLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/brooklyn/location/basic/WinRmMachineLocation.java b/core/src/main/java/brooklyn/location/basic/WinRmMachineLocation.java index ee32568..cb59102 100644 --- a/core/src/main/java/brooklyn/location/basic/WinRmMachineLocation.java +++ b/core/src/main/java/brooklyn/location/basic/WinRmMachineLocation.java @@ -228,6 +228,15 @@ public class WinRmMachineLocation extends AbstractLocation implements MachineLoc } } + @Override + public void init() { + super.init(); + + getRequiredConfig(ADDRESS); + getRequiredConfig(USER); + getRequiredConfig(PASSWORD); + } + public String getUser() { return config().get(USER); } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ae3790ee/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java ---------------------------------------------------------------------- diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java index e0e7990..c665ba7 100644 --- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java +++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java @@ -64,6 +64,7 @@ import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.NodeMetadata.Status; import org.jclouds.compute.domain.NodeMetadataBuilder; +import org.jclouds.compute.domain.OperatingSystem; import org.jclouds.compute.domain.OsFamily; import org.jclouds.compute.domain.Template; import org.jclouds.compute.domain.TemplateBuilder; @@ -321,6 +322,36 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im return (String) config.getWithDeprecation(USER, JCLOUDS_KEY_USERNAME); } + public boolean isWindows(Template template, ConfigBag config) { + return isWindows(template.getImage(), config); + } + + /** + * Whether VMs provisioned from this image will be Windows. Assume windows if the image + * explicitly says so, or if the config explicitly says {@link JcloudsLocationConfig#OS_FAMILY} + * is Windows. + * + * We believe the config (e.g. from brooklyn.properties) because for some clouds there is + * insufficient meta-data so the Image might not tell us. Thus a user can work around it + * by explicitly supplying configuration. + */ + public boolean isWindows(Image image, ConfigBag config) { + OsFamily confFamily = config.get(OS_FAMILY); + OperatingSystem os = (image != null) ? image.getOperatingSystem() : null; + return (os != null) ? (OsFamily.WINDOWS == os.getFamily()) : (OsFamily.WINDOWS == confFamily); + } + + /** + * Whether the given VM is Windows. + * + * @see {@link #isWindows(Image, ConfigBag)} + */ + public boolean isWindows(NodeMetadata node, ConfigBag config) { + OsFamily confFamily = config.get(OS_FAMILY); + OperatingSystem os = (node != null) ? node.getOperatingSystem() : null; + return (os != null) ? (OsFamily.WINDOWS == os.getFamily()) : (OsFamily.WINDOWS == confFamily); + } + protected Semaphore getMachineCreationSemaphore() { return checkNotNull(getConfig(MACHINE_CREATION_SEMAPHORE), MACHINE_CREATION_SEMAPHORE.getName()); } @@ -564,6 +595,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im setCreationString(setup); boolean waitForSshable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_SSHABLE)); + boolean waitForWinRmable = !"false".equalsIgnoreCase(setup.get(WAIT_FOR_WINRM_AVAILABLE)); boolean usePortForwarding = setup.get(USE_PORT_FORWARDING); boolean skipJcloudsSshing = Boolean.FALSE.equals(setup.get(USE_JCLOUDS_SSH_INIT)) || usePortForwarding; JcloudsPortForwarderExtension portForwarder = setup.get(PORT_FORWARDER); @@ -598,8 +630,15 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im try { // Setup the template template = buildTemplate(computeService, setup); - if (waitForSshable && !skipJcloudsSshing) { - userCredentials = initTemplateForCreateUser(template, setup); + boolean expectWindows = isWindows(template, setup); + if (!skipJcloudsSshing) { + if (expectWindows) { + // TODO Was this too early to look at template.getImage? e.g. customizeTemplate could subsequently modify it. + LOG.warn("Ignoring invalid configuration for Windows provisioning of "+template.getImage()+": "+USE_JCLOUDS_SSH_INIT.getName()+" should be false"); + skipJcloudsSshing = true; + } else if (waitForSshable) { + userCredentials = initTemplateForCreateUser(template, setup); + } } templateTimestamp = Duration.of(provisioningStopwatch); @@ -642,24 +681,15 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im if (node == null) throw new IllegalStateException("No nodes returned by jclouds create-nodes in " + setup.getDescription()); - boolean windows = node.getOperatingSystem().getFamily().equals(OsFamily.WINDOWS); + boolean windows = isWindows(node, setup); - if (windows) { - // FIXME Allow for user-specified credentials: - // - want to support setting up a given user on the VM (or resetting the Administrator password) - // - and support vCloudDirector use-case where template contains username/password, so don't just use - // what is returned by jclouds. - setup.put(USER, node.getCredentials().getUser()); - setup.put(PASSWORD, node.getCredentials().getOptionalPassword().orNull()); - } - - // TODO How do we influence the node.getLoginPort, so it is set correctly for Windows? + // FIXME How do we influence the node.getLoginPort, so it is set correctly for Windows? // Setup port-forwarding, if required Optional<HostAndPort> sshHostAndPortOverride; if (usePortForwarding) { sshHostAndPortOverride = Optional.of(portForwarder.openPortForwarding( node, - node.getLoginPort(), + (windows ? 5985 : node.getLoginPort()), Optional.<Integer>absent(), Protocol.TCP, Cidr.UNIVERSAL)); @@ -667,14 +697,15 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im sshHostAndPortOverride = Optional.absent(); } - if (waitForSshable) { - if (windows) { - // TODO Should we generalise `waitForSshable` to a `waitForReachable`? - // TODO Does jclouds support any windows user setup? - waitForWinRmAvailable(node, node.getCredentials(), setup); - } else if (skipJcloudsSshing) { - // once that host:port is definitely reachable, we can create the user - waitForReachable(computeService, node, sshHostAndPortOverride, node.getCredentials(), setup); + if (skipJcloudsSshing) { + boolean waitForConnectable = (windows) ? waitForWinRmable : waitForSshable; + if (waitForConnectable) { + if (windows) { + // TODO Does jclouds support any windows user setup? + waitForWinRmAvailable(computeService, node, sshHostAndPortOverride, node.getCredentials(), setup); + } else { + waitForSshable(computeService, node, sshHostAndPortOverride, node.getCredentials(), setup); + } userCredentials = createUser(computeService, node, sshHostAndPortOverride, setup); } } @@ -704,7 +735,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im // Wait for the VM to be reachable over SSH if (waitForSshable && !windows) { - waitForReachable(computeService, node, sshHostAndPortOverride, userCredentials, setup); + waitForSshable(computeService, node, sshHostAndPortOverride, userCredentials, setup); } else { LOG.debug("Skipping ssh check for {} ({}) due to config waitForSshable=false", node, setup.getDescription()); } @@ -1313,8 +1344,8 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im } TemplateOptions options = template.getOptions(); - OsFamily osFamily = (image.getOperatingSystem() != null) ? image.getOperatingSystem().getFamily() : null; - if (OsFamily.WINDOWS == osFamily) { + boolean windows = isWindows(template, config); + if (windows) { if (!(config.containsKey(JcloudsLocationConfig.USER_METADATA_STRING) || config.containsKey(JcloudsLocationConfig.USER_METADATA_MAP))) { config.put(JcloudsLocationConfig.USER_METADATA_STRING, WinRmMachineLocation.getDefaultUserMetadataString()); } @@ -1397,54 +1428,61 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im : org.jclouds.scriptbuilder.domain.OsFamily.UNIX; } - List<String> commands = Lists.newArrayList(); - for (Statement statement : userCreation.statements) { - InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default()); - initAdminAccess.visit(statement); - commands.add(statement.render(scriptOsFamily)); - } - - LoginCredentials initialCredentials = node.getCredentials(); - Optional<String> initialPassword = initialCredentials.getOptionalPassword(); - Optional<String> initialPrivateKey = initialCredentials.getOptionalPrivateKey(); - String initialUser = initialCredentials.getUser(); - String address = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText() : JcloudsUtil.getFirstReachableAddress(computeService.getContext(), node); - int port = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPort() : node.getLoginPort(); - - Map<String,Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig()); - sshProps.put("user", initialUser); - sshProps.put("address", address); - sshProps.put("port", port); - if (initialPassword.isPresent()) sshProps.put("password", initialPassword.get()); - if (initialPrivateKey.isPresent()) sshProps.put("privateKeyData", initialPrivateKey.get()); - - // TODO Retrying lots of times as workaround for vcloud-director. There the guest customizations - // can cause the VM to reboot shortly after it was ssh'able. - Map<String,Object> execProps = Maps.newLinkedHashMap(); - execProps.put(ShellTool.PROP_RUN_AS_ROOT.getName(), true); - execProps.put(SshTool.PROP_SSH_TRIES.getName(), 50); - execProps.put(SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), 10*60*1000); - - if (LOG.isDebugEnabled()) { - LOG.debug("VM {}: executing user creation/setup via {}@{}:{}; commands: {}", new Object[] { - config.getDescription(), initialUser, address, port, commands}); - } + boolean windows = isWindows(node, config); - SshMachineLocation sshLoc = null; - try { - if (isManaged()) { - sshLoc = getManagementContext().getLocationManager().createLocation(sshProps, SshMachineLocation.class); - } else { - sshLoc = new SshMachineLocation(sshProps); + if (windows) { + LOG.warn("Unable to execute statements on WinRM in JcloudsLocation; skipping for "+node+": "+userCreation.statements); + + } else { + List<String> commands = Lists.newArrayList(); + for (Statement statement : userCreation.statements) { + InitAdminAccess initAdminAccess = new InitAdminAccess(new AdminAccessConfiguration.Default()); + initAdminAccess.visit(statement); + commands.add(statement.render(scriptOsFamily)); } - int exitcode = sshLoc.execScript(execProps, "create-user", commands); - if (exitcode != 0) { - LOG.warn("exit code {} when creating user for {}; usage may subsequently fail", exitcode, node); + LoginCredentials initialCredentials = node.getCredentials(); + Optional<String> initialPassword = initialCredentials.getOptionalPassword(); + Optional<String> initialPrivateKey = initialCredentials.getOptionalPrivateKey(); + String initialUser = initialCredentials.getUser(); + String address = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getHostText() : JcloudsUtil.getFirstReachableAddress(computeService.getContext(), node); + int port = hostAndPortOverride.isPresent() ? hostAndPortOverride.get().getPort() : node.getLoginPort(); + + Map<String,Object> sshProps = Maps.newLinkedHashMap(config.getAllConfig()); + sshProps.put("user", initialUser); + sshProps.put("address", address); + sshProps.put("port", port); + if (initialPassword.isPresent()) sshProps.put("password", initialPassword.get()); + if (initialPrivateKey.isPresent()) sshProps.put("privateKeyData", initialPrivateKey.get()); + + // TODO Retrying lots of times as workaround for vcloud-director. There the guest customizations + // can cause the VM to reboot shortly after it was ssh'able. + Map<String,Object> execProps = Maps.newLinkedHashMap(); + execProps.put(ShellTool.PROP_RUN_AS_ROOT.getName(), true); + execProps.put(SshTool.PROP_SSH_TRIES.getName(), 50); + execProps.put(SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), 10*60*1000); + + if (LOG.isDebugEnabled()) { + LOG.debug("VM {}: executing user creation/setup via {}@{}:{}; commands: {}", new Object[] { + config.getDescription(), initialUser, address, port, commands}); + } + + SshMachineLocation sshLoc = null; + try { + if (isManaged()) { + sshLoc = getManagementContext().getLocationManager().createLocation(sshProps, SshMachineLocation.class); + } else { + sshLoc = new SshMachineLocation(sshProps); + } + + int exitcode = sshLoc.execScript(execProps, "create-user", commands); + if (exitcode != 0) { + LOG.warn("exit code {} when creating user for {}; usage may subsequently fail", exitcode, node); + } + } finally { + getManagementContext().getLocationManager().unmanage(sshLoc); + Streams.closeQuietly(sshLoc); } - } finally { - getManagementContext().getLocationManager().unmanage(sshLoc); - Streams.closeQuietly(sshLoc); } } @@ -1527,7 +1565,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im protected UserCreation createUserStatements(@Nullable Image image, ConfigBag config) { //NB: private key is not installed remotely, just used to get/validate the public key - LoginCredentials createdUserCreds = null; + boolean windows = isWindows(image, config); String user = getUser(config); String explicitLoginUser = config.get(LOGIN_USER); String loginUser = groovyTruth(explicitLoginUser) ? explicitLoginUser : (image != null && image.getDefaultCredentials() != null) ? image.getDefaultCredentials().identity : null; @@ -1537,6 +1575,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im credential.checkNoErrors().logAnyWarnings(); String passwordToSet = Strings.isNonBlank(credential.getPassword()) ? credential.getPassword() : Identifiers.makeRandomId(12); List<Statement> statements = Lists.newArrayList(); + LoginCredentials createdUserCreds = null; if (dontCreateUser) { // dontCreateUser: @@ -1559,6 +1598,22 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im } } + } else if (windows) { + // TODO Generate statements to create the user. + // createdUserCreds returned from this method will be null; + // we will use the creds returned by jclouds on the node + LOG.warn("Not creating or configuring user on Windows VM, despite "+DONT_CREATE_USER.getName()+" set to false"); + + // TODO extractVmCredentials() will use user:publicKeyData defaults, if we don't override this. + // For linux, how would we configure Brooklyn to use the node.getCredentials() - i.e. the version + // that the cloud automatically generated? + if (config.get(USER) != null) config.put(USER, ""); + if (config.get(PASSWORD) != null) config.put(PASSWORD, ""); + if (config.get(PRIVATE_KEY_DATA) != null) config.put(PRIVATE_KEY_DATA, ""); + if (config.get(PRIVATE_KEY_FILE) != null) config.put(PRIVATE_KEY_FILE, ""); + if (config.get(PUBLIC_KEY_DATA) != null) config.put(PUBLIC_KEY_DATA, ""); + if (config.get(PUBLIC_KEY_FILE) != null) config.put(PUBLIC_KEY_FILE, ""); + } else if (Strings.isBlank(user) || user.equals(loginUser) || user.equals(ROOT_USERNAME)) { // For subsequent ssh'ing, we'll be using the loginUser if (Strings.isBlank(user)) { @@ -2186,7 +2241,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im return null; } - private void waitForWinRmAvailable(final NodeMetadata node, final LoginCredentials expectedCredentials, ConfigBag setup) { + protected void waitForWinRmAvailable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, final LoginCredentials expectedCredentials, ConfigBag setup) { String waitForWinrmAvailable = setup.get(WAIT_FOR_WINRM_AVAILABLE); checkArgument(!"false".equalsIgnoreCase(waitForWinrmAvailable), "waitForWinRmAvailable called despite waitForWinRmAvailable=%s", waitForWinrmAvailable); Duration timeout = null; @@ -2195,27 +2250,37 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im } catch (Exception e) { // TODO will this just be a NumberFormatException? If so, catch that specificially // normal if 'true'; just fall back to default + Exceptions.propagateIfFatal(e); } if (timeout == null) { timeout = Duration.parse(WAIT_FOR_WINRM_AVAILABLE.getDefaultValue()); } - // TODO Use getFirstReachableAddress, when have getLoginPort set correctly String user = expectedCredentials.getUser(); String password = expectedCredentials.getOptionalPassword().orNull(); - String vmIp = node.getPublicAddresses().iterator().next(); - final Session session = WinRMFactory.INSTANCE.createSession(vmIp, user, password); + int vmPort = hostAndPortOverride.isPresent() + ? hostAndPortOverride.get().getPortOrDefault(5985) + : 5985; // WinRM port + String vmIp = hostAndPortOverride.isPresent() + ? hostAndPortOverride.get().getHostText() + : JcloudsUtil.getFirstReachableAddress( + computeService.getContext(), + NodeMetadataBuilder.fromNodeMetadata(node).loginPort(vmPort).build()); + + final Session session = WinRMFactory.INSTANCE.createSession(vmIp+":"+vmPort, user, password); + + if (vmIp==null) LOG.warn("Unable to extract IP for "+node+" ("+setup.getDescription()+"): subsequent connection attempt will likely fail"); Callable<Boolean> checker = new Callable<Boolean>() { public Boolean call() { return session.run_cmd("hostname").getStatusCode() == 0; }}; - String connectionDetails = user+"@"+vmIp; + String connectionDetails = user + "@" + vmIp + ":" + vmPort; waitForReachable(checker, connectionDetails, expectedCredentials, setup, timeout); } - protected void waitForReachable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, final LoginCredentials expectedCredentials, ConfigBag setup) { + protected void waitForSshable(final ComputeService computeService, final NodeMetadata node, Optional<HostAndPort> hostAndPortOverride, final LoginCredentials expectedCredentials, ConfigBag setup) { String waitForSshable = setup.get(WAIT_FOR_SSHABLE); checkArgument(!"false".equalsIgnoreCase(waitForSshable), "waitForReachable called despite waitForSshable=%s", waitForSshable);
