From a925d704f4fcb71aae350b8f579333096b655cff Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Fri, 6 May 2011 00:35:50 -0700 Subject: [PATCH] Issue 543, 473: new AdminAccess statement, which locks down compute nodes and creates a default admin account --- .../ec2/compute/EC2ComputeService.java | 14 +- .../TerremarkVCloudComputeService.java | 42 +-- .../BaseComputeServiceContextModule.java | 15 +- .../config/PersistNodeCredentialsModule.java | 97 ++++++ .../compute/internal/BaseComputeService.java | 186 ++++++----- .../internal/PersistNodeCredentials.java | 35 +++ .../compute/BaseComputeServiceLiveTest.java | 101 +++--- .../StubComputeServiceIntegrationTest.java | 81 +++-- .../config/PersistNodeCredentialsTest.java | 147 +++++++++ .../test/resources/initscript_with_java.sh | 30 +- .../main/java/org/jclouds/crypto/Crypto.java | 6 +- .../main/java/org/jclouds/crypto/Pems.java | 118 +++---- .../java/org/jclouds/crypto}/Sha512Crypt.java | 70 +++-- .../main/java/org/jclouds/crypto/SshKeys.java | 111 +++++++ .../encryption/internal/JCECrypto.java | 11 +- .../org/jclouds/util/PasswordGenerator.java | 60 ++++ .../org/jclouds/crypto}/Sha512CryptTest.java | 3 +- .../java/org/jclouds/crypto/SshKeysTest.java | 65 ++++ .../aws/ec2/compute/AWSEC2ComputeService.java | 8 +- .../SlicehostComputeServiceLiveTest.java | 4 +- .../scriptbuilder/domain/StatementList.java | 43 ++- .../functions/CredentialsFromAdminAccess.java | 57 ++++ .../functions/InitAdminAccess.java | 58 ++++ .../statements/login/AdminAccess.java | 295 ++++++++++++++++++ .../login/DefaultConfiguration.java | 84 +++++ .../login/ReplaceShadowPasswordEntry.java | 20 +- .../statements/login/ShadowStatements.java | 3 +- .../statements/login/UserAdd.java | 23 +- .../CredentialsFromAdminAccessTest.java | 84 +++++ .../functions/InitAdminAccessTest.java | 87 ++++++ .../statements/login/AdminAccessTest.java | 79 +++++ .../statements/login/TestConfiguration.java | 84 +++++ .../resources/test_adminaccess_flipped.sh | 21 ++ .../test/resources/test_adminaccess_params.sh | 21 ++ .../resources/test_adminaccess_plainuser.sh | 14 + .../resources/test_adminaccess_standard.sh | 21 ++ 36 files changed, 1903 insertions(+), 295 deletions(-) create mode 100644 compute/src/main/java/org/jclouds/compute/config/PersistNodeCredentialsModule.java create mode 100644 compute/src/main/java/org/jclouds/compute/internal/PersistNodeCredentials.java create mode 100644 compute/src/test/java/org/jclouds/compute/config/PersistNodeCredentialsTest.java rename {scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util => core/src/main/java/org/jclouds/crypto}/Sha512Crypt.java (86%) create mode 100644 core/src/main/java/org/jclouds/crypto/SshKeys.java create mode 100644 core/src/main/java/org/jclouds/util/PasswordGenerator.java rename {scriptbuilder/src/test/java/org/jclouds/scriptbuilder/util => core/src/test/java/org/jclouds/crypto}/Sha512CryptTest.java (97%) create mode 100644 core/src/test/java/org/jclouds/crypto/SshKeysTest.java create mode 100644 scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccess.java create mode 100644 scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/InitAdminAccess.java create mode 100644 scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/AdminAccess.java create mode 100644 scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/DefaultConfiguration.java create mode 100644 scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccessTest.java create mode 100644 scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/InitAdminAccessTest.java create mode 100644 scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/AdminAccessTest.java create mode 100644 scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/TestConfiguration.java create mode 100644 scriptbuilder/src/test/resources/test_adminaccess_flipped.sh create mode 100644 scriptbuilder/src/test/resources/test_adminaccess_params.sh create mode 100644 scriptbuilder/src/test/resources/test_adminaccess_plainuser.sh create mode 100644 scriptbuilder/src/test/resources/test_adminaccess_standard.sh diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ComputeService.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ComputeService.java index 85b2490c74..7cbbb0dd5a 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ComputeService.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ComputeService.java @@ -42,6 +42,7 @@ import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.TemplateBuilder; import org.jclouds.compute.internal.BaseComputeService; +import org.jclouds.compute.internal.PersistNodeCredentials; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.compute.reference.ComputeServiceConstants.Timeouts; import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet; @@ -60,6 +61,7 @@ import org.jclouds.ec2.compute.domain.RegionNameAndIngressRules; import org.jclouds.ec2.compute.options.EC2TemplateOptions; import org.jclouds.ec2.domain.KeyPair; import org.jclouds.ec2.domain.RunningInstance; +import org.jclouds.scriptbuilder.functions.InitAdminAccess; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; @@ -89,13 +91,14 @@ public class EC2ComputeService extends BaseComputeService { @Named("NODE_RUNNING") Predicate nodeRunning, @Named("NODE_TERMINATED") Predicate nodeTerminated, @Named("NODE_SUSPENDED") Predicate nodeSuspended, - InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, Timeouts timeouts, + InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, InitAdminAccess initAdminAccess, + PersistNodeCredentials persistNodeCredentials, Timeouts timeouts, @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, EC2Client ec2Client, Map credentialsMap, @Named("SECURITY") Map securityGroupMap) { super(context, credentialStore, images, sizes, locations, listNodesStrategy, getNodeMetadataStrategy, runNodesAndAddToSetStrategy, rebootNodeStrategy, destroyNodeStrategy, startNodeStrategy, stopNodeStrategy, templateBuilderProvider, templateOptionsProvider, nodeRunning, nodeTerminated, nodeSuspended, - initScriptRunnerFactory, timeouts, executor); + initScriptRunnerFactory, initAdminAccess, persistNodeCredentials, timeouts, executor); this.ec2Client = ec2Client; this.credentialsMap = credentialsMap; this.securityGroupMap = securityGroupMap; @@ -125,7 +128,8 @@ public class EC2ComputeService extends BaseComputeService { // when the keypair is unique per group keyPair.getKeyName().equals("jclouds#" + group) || keyPair.getKeyName().matches(String.format("jclouds#%s#%s", group, "[0-9a-f]+")) - // old keypair pattern too verbose as it has an unnecessary region qualifier + // old keypair pattern too verbose as it has an unnecessary + // region qualifier || keyPair.getKeyName().matches(String.format("jclouds#%s#%s#%s", group, region, "[0-9a-f]+"))) { Set instancesUsingKeyPair = extractIdsFromInstances(filter(concat(ec2Client.getInstanceServices() .describeInstancesInRegion(region)), usingKeyPairAndNotDead(keyPair))); @@ -170,8 +174,8 @@ public class EC2ComputeService extends BaseComputeService { } /** - * like {@link BaseComputeService#destroyNodesMatching} except that this will clean implicit - * keypairs and security groups. + * like {@link BaseComputeService#destroyNodesMatching} except that this will + * clean implicit keypairs and security groups. */ @Override public Set destroyNodesMatching(Predicate filter) { diff --git a/common/trmk/src/main/java/org/jclouds/vcloud/terremark/compute/TerremarkVCloudComputeService.java b/common/trmk/src/main/java/org/jclouds/vcloud/terremark/compute/TerremarkVCloudComputeService.java index 15b9eac8d1..2b06e57639 100644 --- a/common/trmk/src/main/java/org/jclouds/vcloud/terremark/compute/TerremarkVCloudComputeService.java +++ b/common/trmk/src/main/java/org/jclouds/vcloud/terremark/compute/TerremarkVCloudComputeService.java @@ -36,18 +36,20 @@ import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.TemplateBuilder; import org.jclouds.compute.internal.BaseComputeService; +import org.jclouds.compute.internal.PersistNodeCredentials; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.compute.reference.ComputeServiceConstants.Timeouts; +import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet; import org.jclouds.compute.strategy.DestroyNodeStrategy; import org.jclouds.compute.strategy.GetNodeMetadataStrategy; import org.jclouds.compute.strategy.InitializeRunScriptOnNodeOrPlaceInBadMap; import org.jclouds.compute.strategy.ListNodesStrategy; import org.jclouds.compute.strategy.RebootNodeStrategy; import org.jclouds.compute.strategy.ResumeNodeStrategy; -import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet; import org.jclouds.compute.strategy.SuspendNodeStrategy; import org.jclouds.domain.Credentials; import org.jclouds.domain.Location; +import org.jclouds.scriptbuilder.functions.InitAdminAccess; import org.jclouds.vcloud.terremark.compute.domain.KeyPairCredentials; import org.jclouds.vcloud.terremark.compute.domain.OrgAndName; import org.jclouds.vcloud.terremark.compute.functions.NodeMetadataToOrgAndName; @@ -66,28 +68,29 @@ public class TerremarkVCloudComputeService extends BaseComputeService { @Inject protected TerremarkVCloudComputeService(ComputeServiceContext context, Map credentialStore, - @Memoized Supplier> images, @Memoized Supplier> sizes, - @Memoized Supplier> locations, ListNodesStrategy listNodesStrategy, - GetNodeMetadataStrategy getNodeMetadataStrategy, CreateNodesInGroupThenAddToSet runNodesAndAddToSetStrategy, - RebootNodeStrategy rebootNodeStrategy, DestroyNodeStrategy destroyNodeStrategy, - ResumeNodeStrategy resumeNodeStrategy, SuspendNodeStrategy suspendNodeStrategy, - Provider templateBuilderProvider, Provider templateOptionsProvider, - @Named("NODE_RUNNING") Predicate nodeRunning, - @Named("NODE_TERMINATED") Predicate nodeTerminated, - @Named("NODE_SUSPENDED") Predicate nodeSuspended, - InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, Timeouts timeouts, - @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, CleanupOrphanKeys cleanupOrphanKeys, - ConcurrentMap credentialsMap, NodeMetadataToOrgAndName nodeToOrgAndName) { + @Memoized Supplier> images, @Memoized Supplier> sizes, + @Memoized Supplier> locations, ListNodesStrategy listNodesStrategy, + GetNodeMetadataStrategy getNodeMetadataStrategy, CreateNodesInGroupThenAddToSet runNodesAndAddToSetStrategy, + RebootNodeStrategy rebootNodeStrategy, DestroyNodeStrategy destroyNodeStrategy, + ResumeNodeStrategy resumeNodeStrategy, SuspendNodeStrategy suspendNodeStrategy, + Provider templateBuilderProvider, Provider templateOptionsProvider, + @Named("NODE_RUNNING") Predicate nodeRunning, + @Named("NODE_TERMINATED") Predicate nodeTerminated, + @Named("NODE_SUSPENDED") Predicate nodeSuspended, + InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, InitAdminAccess initAdminAccess, + PersistNodeCredentials persistNodeCredentials, Timeouts timeouts, + @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, CleanupOrphanKeys cleanupOrphanKeys, + ConcurrentMap credentialsMap, NodeMetadataToOrgAndName nodeToOrgAndName) { super(context, credentialStore, images, sizes, locations, listNodesStrategy, getNodeMetadataStrategy, - runNodesAndAddToSetStrategy, rebootNodeStrategy, destroyNodeStrategy, resumeNodeStrategy, - suspendNodeStrategy, templateBuilderProvider, templateOptionsProvider, nodeRunning, nodeTerminated, - nodeSuspended, initScriptRunnerFactory, timeouts, executor); + runNodesAndAddToSetStrategy, rebootNodeStrategy, destroyNodeStrategy, resumeNodeStrategy, + suspendNodeStrategy, templateBuilderProvider, templateOptionsProvider, nodeRunning, nodeTerminated, + nodeSuspended, initScriptRunnerFactory, initAdminAccess, persistNodeCredentials, timeouts, executor); this.cleanupOrphanKeys = cleanupOrphanKeys; } /** - * like {@link BaseComputeService#destroyNodesMatching} except that this will clean implicit - * keypairs. + * like {@link BaseComputeService#destroyNodesMatching} except that this will + * clean implicit keypairs. */ @Override public Set destroyNodesMatching(Predicate filter) { @@ -97,7 +100,8 @@ public class TerremarkVCloudComputeService extends BaseComputeService { } /** - * returns template options, except of type {@link TerremarkVCloudTemplateOptions}. + * returns template options, except of type + * {@link TerremarkVCloudTemplateOptions}. */ @Override public TerremarkVCloudTemplateOptions templateOptions() { diff --git a/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java b/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java index 1cddb87180..78c319e67f 100644 --- a/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java +++ b/compute/src/main/java/org/jclouds/compute/config/BaseComputeServiceContextModule.java @@ -73,6 +73,7 @@ import com.google.inject.name.Names; * @author Adrian Cole */ public abstract class BaseComputeServiceContextModule extends AbstractModule { + @Override protected void configure() { install(new LocationModule(authException)); @@ -89,6 +90,8 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { .implement(RunScriptOnNode.class, Names.named("nonblocking"), RunScriptOnNodeAsInitScriptUsingSsh.class) .build(RunScriptOnNodeFactoryImpl.Factory.class)); + install(new PersistNodeCredentialsModule()); + bind(RunScriptOnNode.Factory.class).to(RunScriptOnNodeFactoryImpl.class); install(new FactoryModuleBuilder().implement(new TypeLiteral>() { @@ -170,8 +173,8 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { } /** - * supplies how the tag is encoded into the name. A string of hex characters is the last argument - * and tag is the first + * supplies how the tag is encoded into the name. A string of hex characters + * is the last argument and tag is the first */ @Provides @Named("NAMING_CONVENTION") @@ -207,8 +210,8 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { @Memoized protected Supplier> supplyImageCache(@Named(PROPERTY_SESSION_INTERVAL) long seconds, final Supplier> imageSupplier) { - return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier>(authException, seconds, - new Supplier>() { + return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier>(authException, + seconds, new Supplier>() { @Override public Set get() { return imageSupplier.get(); @@ -241,8 +244,8 @@ public abstract class BaseComputeServiceContextModule extends AbstractModule { @Memoized protected Supplier> supplySizeCache(@Named(PROPERTY_SESSION_INTERVAL) long seconds, final Supplier> hardwareSupplier) { - return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier>(authException, seconds, - new Supplier>() { + return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier>(authException, + seconds, new Supplier>() { @Override public Set get() { return hardwareSupplier.get(); diff --git a/compute/src/main/java/org/jclouds/compute/config/PersistNodeCredentialsModule.java b/compute/src/main/java/org/jclouds/compute/config/PersistNodeCredentialsModule.java new file mode 100644 index 0000000000..23396e81ee --- /dev/null +++ b/compute/src/main/java/org/jclouds/compute/config/PersistNodeCredentialsModule.java @@ -0,0 +1,97 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.compute.config; + +import java.util.Map; + +import javax.annotation.Nullable; + +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeMetadataBuilder; +import org.jclouds.compute.internal.PersistNodeCredentials; +import org.jclouds.domain.Credentials; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.functions.CredentialsFromAdminAccess; + +import com.google.common.base.Function; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.name.Names; + +/** + * + * @author Adrian Cole + */ +public class PersistNodeCredentialsModule extends AbstractModule { + + static class RefreshCredentialsForNodeIfRanAdminAccess implements Function { + protected final Map credentialStore; + protected final Statement statement; + + @Inject + protected RefreshCredentialsForNodeIfRanAdminAccess(Map credentialStore, + @Nullable @Assisted Statement statement) { + this.credentialStore = credentialStore; + this.statement = statement; + } + + @Override + public NodeMetadata apply(NodeMetadata input) { + if (statement == null) + return input; + Credentials credentials = CredentialsFromAdminAccess.INSTANCE.apply(statement); + if (credentials != null) { + input = NodeMetadataBuilder.fromNodeMetadata(input).credentials(credentials).build(); + credentialStore.put("node#" + input.getId(), input.getCredentials()); + } + return input; + } + + } + + static class RefreshCredentialsForNode extends RefreshCredentialsForNodeIfRanAdminAccess { + + @Inject + public RefreshCredentialsForNode(Map credentialStore, @Assisted @Nullable Statement statement) { + super(credentialStore, statement); + } + + @Override + public NodeMetadata apply(NodeMetadata input) { + input = super.apply(input); + if (input.getCredentials() != null) + credentialStore.put("node#" + input.getId(), input.getCredentials()); + return input; + } + + } + + @Override + protected void configure() { + + install(new FactoryModuleBuilder().implement(new TypeLiteral>() { + }, Names.named("ifAdminAccess"), RefreshCredentialsForNodeIfRanAdminAccess.class) + .implement(new TypeLiteral>() { + }, Names.named("always"), RefreshCredentialsForNode.class).build(PersistNodeCredentials.class)); + } + +} \ No newline at end of file diff --git a/compute/src/main/java/org/jclouds/compute/internal/BaseComputeService.java b/compute/src/main/java/org/jclouds/compute/internal/BaseComputeService.java index c4ce30216f..a28a47d790 100755 --- a/compute/src/main/java/org/jclouds/compute/internal/BaseComputeService.java +++ b/compute/src/main/java/org/jclouds/compute/internal/BaseComputeService.java @@ -22,7 +22,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.not; import static com.google.common.base.Predicates.notNull; -import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Maps.newLinkedHashMap; import static com.google.common.collect.Sets.filter; @@ -86,6 +85,8 @@ import org.jclouds.logging.Logger; import org.jclouds.predicates.RetryablePredicate; import org.jclouds.scriptbuilder.domain.Statement; import org.jclouds.scriptbuilder.domain.Statements; +import org.jclouds.scriptbuilder.functions.InitAdminAccess; +import org.jclouds.util.Maps2; import org.jclouds.util.Strings2; import com.google.common.base.Function; @@ -93,6 +94,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; @@ -128,23 +130,24 @@ public class BaseComputeService implements ComputeService { private final Predicate nodeSuspended; private final InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory; private final Timeouts timeouts; + private final InitAdminAccess initAdminAccess; + private final PersistNodeCredentials persistNodeCredentials; private final ExecutorService executor; @Inject protected BaseComputeService(ComputeServiceContext context, Map credentialStore, - @Memoized Supplier> images, - @Memoized Supplier> hardwareProfiles, - @Memoized Supplier> locations, ListNodesStrategy listNodesStrategy, - GetNodeMetadataStrategy getNodeMetadataStrategy, - CreateNodesInGroupThenAddToSet runNodesAndAddToSetStrategy, RebootNodeStrategy rebootNodeStrategy, - DestroyNodeStrategy destroyNodeStrategy, ResumeNodeStrategy resumeNodeStrategy, - SuspendNodeStrategy suspendNodeStrategy, Provider templateBuilderProvider, - Provider templateOptionsProvider, - @Named("NODE_RUNNING") Predicate nodeRunning, - @Named("NODE_TERMINATED") Predicate nodeTerminated, - @Named("NODE_SUSPENDED") Predicate nodeSuspended, - InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, Timeouts timeouts, - @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor) { + @Memoized Supplier> images, @Memoized Supplier> hardwareProfiles, + @Memoized Supplier> locations, ListNodesStrategy listNodesStrategy, + GetNodeMetadataStrategy getNodeMetadataStrategy, CreateNodesInGroupThenAddToSet runNodesAndAddToSetStrategy, + RebootNodeStrategy rebootNodeStrategy, DestroyNodeStrategy destroyNodeStrategy, + ResumeNodeStrategy resumeNodeStrategy, SuspendNodeStrategy suspendNodeStrategy, + Provider templateBuilderProvider, Provider templateOptionsProvider, + @Named("NODE_RUNNING") Predicate nodeRunning, + @Named("NODE_TERMINATED") Predicate nodeTerminated, + @Named("NODE_SUSPENDED") Predicate nodeSuspended, + InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, InitAdminAccess initAdminAccess, + PersistNodeCredentials persistNodeCredentials, Timeouts timeouts, + @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor) { this.context = checkNotNull(context, "context"); this.credentialStore = checkNotNull(credentialStore, "credentialStore"); this.images = checkNotNull(images, "images"); @@ -164,6 +167,8 @@ public class BaseComputeService implements ComputeService { this.nodeSuspended = checkNotNull(nodeSuspended, "nodeSuspended"); this.initScriptRunnerFactory = checkNotNull(initScriptRunnerFactory, "initScriptRunnerFactory"); this.timeouts = checkNotNull(timeouts, "timeouts"); + this.initAdminAccess = checkNotNull(initAdminAccess, "initAdminAccess"); + this.persistNodeCredentials = checkNotNull(persistNodeCredentials, "persistNodeCredentials"); this.executor = checkNotNull(executor, "executor"); } @@ -180,7 +185,7 @@ public class BaseComputeService implements ComputeService { */ @Override public Set runNodesWithTag(String group, int count, Template template) - throws RunNodesException { + throws RunNodesException { return createNodesInGroup(group, count, template); } @@ -189,7 +194,7 @@ public class BaseComputeService implements ComputeService { */ @Override public Set runNodesWithTag(String group, int count, TemplateOptions templateOptions) - throws RunNodesException { + throws RunNodesException { return createNodesInGroup(group, count, templateBuilder().any().options(templateOptions).build()); } @@ -203,23 +208,26 @@ public class BaseComputeService implements ComputeService { @Override public Set createNodesInGroup(String group, int count, Template template) - throws RunNodesException { + throws RunNodesException { checkNotNull(group, "group cannot be null"); checkNotNull(template.getLocation(), "location"); logger.debug(">> running %d node%s group(%s) location(%s) image(%s) hardwareProfile(%s) options(%s)", count, - count > 1 ? "s" : "", group, template.getLocation().getId(), template.getImage().getId(), template - .getHardware().getId(), template.getOptions()); + count > 1 ? "s" : "", group, template.getLocation().getId(), template.getImage().getId(), template + .getHardware().getId(), template.getOptions()); Set goodNodes = newLinkedHashSet(); Map badNodes = newLinkedHashMap(); Multimap customizationResponses = LinkedHashMultimap.create(); + if (template.getOptions().getRunScript() != null) + template.getOptions().runScript(initAdminAccess.apply(template.getOptions().getRunScript())); + Map> responses = runNodesAndAddToSetStrategy.execute(group, count, template, goodNodes, badNodes, - customizationResponses); + customizationResponses); Map executionExceptions = awaitCompletion(responses, executor, null, logger, "runNodesWithTag(" - + group + ")"); - for (NodeMetadata node : concat(goodNodes, badNodes.keySet())) - if (node.getCredentials() != null) - credentialStore.put("node#" + node.getId(), node.getCredentials()); + + group + ")"); + Function fn = persistNodeCredentials.always(template.getOptions().getRunScript()); + badNodes = Maps2.transformKeys(badNodes, fn); + goodNodes = ImmutableSet.copyOf(Iterables.transform(goodNodes, fn)); if (executionExceptions.size() > 0 || badNodes.size() > 0) { throw new RunNodesException(group, count, template, goodNodes, executionExceptions, badNodes); } @@ -228,9 +236,8 @@ public class BaseComputeService implements ComputeService { @Override public Set createNodesInGroup(String group, int count, TemplateOptions templateOptions) - throws RunNodesException { + throws RunNodesException { return createNodesInGroup(group, count, templateBuilder().any().options(templateOptions).build()); - } @Override @@ -275,27 +282,27 @@ public class BaseComputeService implements ComputeService { public Set destroyNodesMatching(Predicate filter) { logger.debug(">> destroying nodes matching(%s)", filter); Set set = newLinkedHashSet(transformParallel(nodesMatchingFilterAndNotTerminated(filter), - new Function>() { + new Function>() { - // TODO make an async interface instead of re-wrapping - @Override - public Future apply(final NodeMetadata from) { - return executor.submit(new Callable() { + // TODO make an async interface instead of re-wrapping + @Override + public Future apply(final NodeMetadata from) { + return executor.submit(new Callable() { - @Override - public NodeMetadata call() throws Exception { - destroyNode(from.getId()); - return from; - } + @Override + public NodeMetadata call() throws Exception { + destroyNode(from.getId()); + return from; + } - @Override - public String toString() { - return "destroyNode(" + from.getId() + ")"; - } - }); - } + @Override + public String toString() { + return "destroyNode(" + from.getId() + ")"; + } + }); + } - }, executor, null, logger, "destroyNodesMatching(" + filter + ")")); + }, executor, null, logger, "destroyNodesMatching(" + filter + ")")); logger.debug("<< destroyed(%d)", set.size()); return set; } @@ -309,7 +316,7 @@ public class BaseComputeService implements ComputeService { * if none found */ Iterable nodesMatchingFilterAndNotTerminatedExceptionIfNotFound( - Predicate filter) { + Predicate filter) { Iterable nodes = nodesMatchingFilterAndNotTerminated(filter); if (Iterables.size(nodes) == 0) throw new NoSuchElementException("no nodes matched filter: " + filter); @@ -399,15 +406,15 @@ public class BaseComputeService implements ComputeService { public void rebootNodesMatching(Predicate filter) { logger.debug(">> rebooting nodes matching(%s)", filter); transformParallel(nodesMatchingFilterAndNotTerminatedExceptionIfNotFound(filter), - new Function>() { - // TODO use native async - @Override - public Future apply(NodeMetadata from) { - rebootNode(from.getId()); - return immediateFuture(null); - } + new Function>() { + // TODO use native async + @Override + public Future apply(NodeMetadata from) { + rebootNode(from.getId()); + return immediateFuture(null); + } - }, executor, null, logger, "rebootNodesMatching(" + filter + ")"); + }, executor, null, logger, "rebootNodesMatching(" + filter + ")"); logger.debug("<< rebooted"); } @@ -430,15 +437,15 @@ public class BaseComputeService implements ComputeService { public void resumeNodesMatching(Predicate filter) { logger.debug(">> resuming nodes matching(%s)", filter); transformParallel(nodesMatchingFilterAndNotTerminatedExceptionIfNotFound(filter), - new Function>() { - // TODO use native async - @Override - public Future apply(NodeMetadata from) { - resumeNode(from.getId()); - return immediateFuture(null); - } + new Function>() { + // TODO use native async + @Override + public Future apply(NodeMetadata from) { + resumeNode(from.getId()); + return immediateFuture(null); + } - }, executor, null, logger, "resumeNodesMatching(" + filter + ")"); + }, executor, null, logger, "resumeNodesMatching(" + filter + ")"); logger.debug("<< resumed"); } @@ -461,15 +468,15 @@ public class BaseComputeService implements ComputeService { public void suspendNodesMatching(Predicate filter) { logger.debug(">> suspending nodes matching(%s)", filter); transformParallel(nodesMatchingFilterAndNotTerminatedExceptionIfNotFound(filter), - new Function>() { - // TODO use native async - @Override - public Future apply(NodeMetadata from) { - suspendNode(from.getId()); - return immediateFuture(null); - } + new Function>() { + // TODO use native async + @Override + public Future apply(NodeMetadata from) { + suspendNode(from.getId()); + return immediateFuture(null); + } - }, executor, null, logger, "suspendNodesMatching(" + filter + ")"); + }, executor, null, logger, "suspendNodesMatching(" + filter + ")"); logger.debug("<< suspended"); } @@ -478,7 +485,7 @@ public class BaseComputeService implements ComputeService { */ @Override public Map runScriptOnNodesMatching(Predicate filter, Payload runScript) - throws RunScriptOnNodesException { + throws RunScriptOnNodesException { return runScriptOnNodesMatching(filter, runScript, RunScriptOptions.NONE); } @@ -487,10 +494,10 @@ public class BaseComputeService implements ComputeService { */ @Override public Map runScriptOnNodesMatching(Predicate filter, Payload runScript, - RunScriptOptions options) throws RunScriptOnNodesException { + RunScriptOptions options) throws RunScriptOnNodesException { try { - return runScriptOnNodesMatching(filter, Statements.exec(Strings2.toStringAndClose(checkNotNull(runScript, - "runScript").getInput())), options); + return runScriptOnNodesMatching(filter, + Statements.exec(Strings2.toStringAndClose(checkNotNull(runScript, "runScript").getInput())), options); } catch (IOException e) { Throwables.propagate(e); return null; @@ -502,7 +509,7 @@ public class BaseComputeService implements ComputeService { */ @Override public Map runScriptOnNodesMatching(Predicate filter, String runScript) - throws RunScriptOnNodesException { + throws RunScriptOnNodesException { return runScriptOnNodesMatching(filter, Statements.exec(checkNotNull(runScript, "runScript"))); } @@ -511,15 +518,15 @@ public class BaseComputeService implements ComputeService { */ @Override public Map runScriptOnNodesMatching(Predicate filter, Statement runScript) - throws RunScriptOnNodesException { + throws RunScriptOnNodesException { return runScriptOnNodesMatching(filter, runScript, RunScriptOptions.NONE); } @Override public Map runScriptOnNodesMatching(Predicate filter, - String runScript, RunScriptOptions options) throws RunScriptOnNodesException { + String runScript, RunScriptOptions options) throws RunScriptOnNodesException { return runScriptOnNodesMatching(filter, Statements.exec(checkNotNull(runScript, "runScript")), - RunScriptOptions.NONE); + RunScriptOptions.NONE); } /** @@ -527,7 +534,7 @@ public class BaseComputeService implements ComputeService { */ @Override public Map runScriptOnNodesMatching(Predicate filter, Statement runScript, - RunScriptOptions options) throws RunScriptOnNodesException { + RunScriptOptions options) throws RunScriptOnNodesException { checkNotNull(filter, "filter"); checkNotNull(runScript, "runScript"); @@ -538,16 +545,22 @@ public class BaseComputeService implements ComputeService { Map> responses = newLinkedHashMap(); Map exceptions = ImmutableMap. of(); + runScript = initAdminAccess.apply(runScript); + Iterable scriptRunners = transformNodesIntoInitializedScriptRunners( - nodesMatchingFilterAndNotTerminatedExceptionIfNotFound(filter), runScript, options, badNodes); + nodesMatchingFilterAndNotTerminatedExceptionIfNotFound(filter), runScript, options, badNodes); if (Iterables.size(scriptRunners) > 0) { for (RunScriptOnNode runner : scriptRunners) { responses.put(runner.getNode(), executor.submit(new RunScriptOnNodeAndAddToGoodMapOrPutExceptionIntoBadMap( - runner, goodNodes, badNodes))); + runner, goodNodes, badNodes))); } exceptions = awaitCompletion(responses, executor, null, logger, "runScriptOnNodesMatching(" + filter + ")"); } + Function fn = persistNodeCredentials.ifAdminAccess(runScript); + badNodes = Maps2.transformKeys(badNodes, fn); + goodNodes = Maps2.transformKeys(goodNodes, fn); + if (exceptions.size() > 0 || badNodes.size() > 0) { throw new RunScriptOnNodesException(runScript, options, goodNodes, exceptions, badNodes); } @@ -555,10 +568,11 @@ public class BaseComputeService implements ComputeService { } private Iterable transformNodesIntoInitializedScriptRunners( - Iterable nodes, Statement script, RunScriptOptions options, - Map badNodes) { - return filter(transformParallel(nodes, new TransformNodesIntoInitializedScriptRunners(script, options, badNodes), - executor, null, logger, "initialize script runners"), notNull()); + Iterable nodes, Statement script, RunScriptOptions options, + Map badNodes) { + return filter( + transformParallel(nodes, new TransformNodesIntoInitializedScriptRunners(script, options, badNodes), + executor, null, logger, "initialize script runners"), notNull()); } private Set detailsOnAllNodes() { @@ -571,13 +585,13 @@ public class BaseComputeService implements ComputeService { } private final class TransformNodesIntoInitializedScriptRunners implements - Function> { + Function> { private final Map badNodes; private final Statement script; private final RunScriptOptions options; private TransformNodesIntoInitializedScriptRunners(Statement script, RunScriptOptions options, - Map badNodes) { + Map badNodes) { this.badNodes = checkNotNull(badNodes, "badNodes"); this.script = checkNotNull(script, "script"); this.options = checkNotNull(options, "options"); @@ -588,7 +602,7 @@ public class BaseComputeService implements ComputeService { checkNotNull(node, "node"); if (options.getOverridingCredentials() != null) { Builder builder = node.getCredentials() != null ? node.getCredentials().toBuilder() - : new Credentials.Builder(); + : new Credentials.Builder(); if (options.getOverridingCredentials().identity != null) builder.identity(options.getOverridingCredentials().identity); if (options.getOverridingCredentials().credential != null) diff --git a/compute/src/main/java/org/jclouds/compute/internal/PersistNodeCredentials.java b/compute/src/main/java/org/jclouds/compute/internal/PersistNodeCredentials.java new file mode 100644 index 0000000000..b1d205ea8f --- /dev/null +++ b/compute/src/main/java/org/jclouds/compute/internal/PersistNodeCredentials.java @@ -0,0 +1,35 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.compute.internal; + +import javax.annotation.Nullable; +import javax.inject.Named; + +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.scriptbuilder.domain.Statement; + +import com.google.common.base.Function; + +public interface PersistNodeCredentials { + @Named("ifAdminAccess") + Function ifAdminAccess(@Nullable Statement statement); + + @Named("always") + Function always(@Nullable Statement statement); +} \ No newline at end of file diff --git a/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java b/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java index 6561325afc..b2611b7fd8 100755 --- a/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java +++ b/compute/src/test/java/org/jclouds/compute/BaseComputeServiceLiveTest.java @@ -44,12 +44,12 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collection; import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -77,6 +77,7 @@ import org.jclouds.predicates.SocketOpen; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.RestContextFactory; import org.jclouds.scriptbuilder.domain.Statements; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; import org.jclouds.ssh.SshClient; import org.jclouds.ssh.SshException; import org.testng.annotations.AfterTest; @@ -159,8 +160,8 @@ public abstract class BaseComputeServiceLiveTest { if (context != null) context.close(); Properties props = setupProperties(); - context = new ComputeServiceContextFactory(setupRestProperties()).createContext(provider, ImmutableSet.of( - new Log4JLoggingModule(), getSshModule()), props); + context = new ComputeServiceContextFactory(setupRestProperties()).createContext(provider, + ImmutableSet.of(new Log4JLoggingModule(), getSshModule()), props); client = context.getComputeService(); } @@ -180,8 +181,8 @@ public abstract class BaseComputeServiceLiveTest { public void testCorrectAuthException() throws Exception { ComputeServiceContext context = null; try { - context = new ComputeServiceContextFactory(setupRestProperties()).createContext(provider, "MOMMA", "MIA", ImmutableSet - . of(new Log4JLoggingModule())); + context = new ComputeServiceContextFactory(setupRestProperties()).createContext(provider, "MOMMA", "MIA", + ImmutableSet. of(new Log4JLoggingModule())); context.getComputeService().listNodes(); } catch (AuthorizationException e) { throw e; @@ -206,7 +207,7 @@ public abstract class BaseComputeServiceLiveTest { @Test(enabled = true, expectedExceptions = NoSuchElementException.class) public void testCorrectExceptionRunningNodesNotFound() throws Exception { client.runScriptOnNodesMatching(runningInGroup("zebras-are-awesome"), buildScript(new OperatingSystem.Builder() - .family(OsFamily.UBUNTU).description("ffoo").build())); + .family(OsFamily.UBUNTU).description("ffoo").build())); } // since surefire and eclipse don't otherwise guarantee the order, we are @@ -230,17 +231,17 @@ public abstract class BaseComputeServiceLiveTest { OperatingSystem os = get(nodes, 0).getOperatingSystem(); try { Map responses = runScriptWithCreds(group, os, new Credentials( - good.identity, "romeo")); + good.identity, "romeo")); assert false : "shouldn't pass with a bad password\n" + responses; } catch (RunScriptOnNodesException e) { assert getRootCause(e).getMessage().contains("Auth fail") : e; } for (Entry response : client.runScriptOnNodesMatching( - runningInGroup(group), Statements.exec("echo hello"), - overrideCredentialsWith(good).wrapInInitScript(false).runAsRoot(false)).entrySet()) + runningInGroup(group), Statements.exec("echo hello"), + overrideCredentialsWith(good).wrapInInitScript(false).runAsRoot(false)).entrySet()) assert response.getValue().getOutput().trim().equals("hello") : response.getKey() + ": " - + response.getValue(); + + response.getValue(); runScriptWithCreds(group, os, good); @@ -294,8 +295,10 @@ public abstract class BaseComputeServiceLiveTest { private void refreshTemplate() { template = buildTemplate(client.templateBuilder()); - template.getOptions().installPrivateKey(keyPair.get("private")).authorizePublicKey(keyPair.get("public")) - .runScript(buildScript(template.getImage().getOperatingSystem())); + // template.getOptions().installPrivateKey(keyPair.get("private")).authorizePublicKey(keyPair.get("public")) + // .runScript(buildScript(template.getImage().getOperatingSystem())); + template.getOptions().runScript( + Statements.newStatementList(AdminAccess.standard(), buildScript(template.getImage().getOperatingSystem()))); } protected void checkImageIdMatchesTemplate(NodeMetadata node) { @@ -306,8 +309,8 @@ public abstract class BaseComputeServiceLiveTest { protected void checkOsMatchesTemplate(NodeMetadata node) { if (node.getOperatingSystem() != null) assert node.getOperatingSystem().getFamily().equals(template.getImage().getOperatingSystem().getFamily()) : String - .format("expecting family %s but got %s", template.getImage().getOperatingSystem().getFamily(), node - .getOperatingSystem()); + .format("expecting family %s but got %s", template.getImage().getOperatingSystem().getFamily(), + node.getOperatingSystem()); } void assertLocationSameOrChild(Location test, Location expected) { @@ -339,10 +342,10 @@ public abstract class BaseComputeServiceLiveTest { } protected Map runScriptWithCreds(final String group, OperatingSystem os, - Credentials creds) throws RunScriptOnNodesException { + Credentials creds) throws RunScriptOnNodesException { try { return client.runScriptOnNodesMatching(runningInGroup(group), buildScript(os), overrideCredentialsWith(creds) - .nameTask("runScriptWithCreds")); + .nameTask("runScriptWithCreds")); } catch (SshException e) { throw e; } @@ -372,16 +375,16 @@ public abstract class BaseComputeServiceLiveTest { @Test(enabled = true, dependsOnMethods = "testCreateAnotherNodeWithANewContextToEnsureSharedMemIsntRequired") public void testGet() throws Exception { - Map metadataMap = newLinkedHashMap(uniqueIndex(filter(client - .listNodesDetailsMatching(all()), and(inGroup(group), not(TERMINATED))), - new Function() { + Map metadataMap = newLinkedHashMap(uniqueIndex( + filter(client.listNodesDetailsMatching(all()), and(inGroup(group), not(TERMINATED))), + new Function() { - @Override - public String apply(NodeMetadata from) { - return from.getId(); - } + @Override + public String apply(NodeMetadata from) { + return from.getId(); + } - })); + })); for (NodeMetadata node : nodes) { metadataMap.remove(node.getId()); NodeMetadata metadata = client.getNodeMetadata(node.getId()); @@ -400,7 +403,7 @@ public abstract class BaseComputeServiceLiveTest { protected void assertNodeZero(Collection metadataSet) { assert metadataSet.size() == 0 : String.format("nodes left in set: [%s] which didn't match set: [%s]", - metadataSet, nodes); + metadataSet, nodes); } @Test(enabled = true, dependsOnMethods = "testGet") @@ -488,12 +491,12 @@ public abstract class BaseComputeServiceLiveTest { } template = client.templateBuilder().options(blockOnComplete(false).blockOnPort(8080, 600).inboundPorts(22, 8080)) - .build(); + .build(); // note this is a dependency on the template resolution template.getOptions().runScript( - RunScriptData.createScriptInstallAndStartJBoss(keyPair.get("public"), template.getImage() - .getOperatingSystem())); + RunScriptData.createScriptInstallAndStartJBoss(keyPair.get("public"), template.getImage() + .getOperatingSystem())); try { NodeMetadata node = getOnlyElement(client.createNodesInGroup(group, 1, template)); @@ -527,26 +530,26 @@ public abstract class BaseComputeServiceLiveTest { assert location != location.getParent() : location; assert location.getScope() != null : location; switch (location.getScope()) { - case PROVIDER: - assertProvider(location); - break; - case REGION: - assertProvider(location.getParent()); - break; - case ZONE: - Location provider = location.getParent().getParent(); - // zone can be a direct descendant of provider - if (provider == null) - provider = location.getParent(); - assertProvider(provider); - break; - case HOST: - Location provider2 = location.getParent().getParent().getParent(); - // zone can be a direct descendant of provider - if (provider2 == null) - provider2 = location.getParent().getParent(); - assertProvider(provider2); - break; + case PROVIDER: + assertProvider(location); + break; + case REGION: + assertProvider(location.getParent()); + break; + case ZONE: + Location provider = location.getParent().getParent(); + // zone can be a direct descendant of provider + if (provider == null) + provider = location.getParent(); + assertProvider(provider); + break; + case HOST: + Location provider2 = location.getParent().getParent().getParent(); + // zone can be a direct descendant of provider + if (provider2 == null) + provider2 = location.getParent().getParent(); + assertProvider(provider2); + break; } } } @@ -634,7 +637,7 @@ public abstract class BaseComputeServiceLiveTest { assertEquals(hello.getOutput().trim(), "hello"); ExecResponse exec = ssh.exec("java -version"); assert exec.getError().indexOf("1.6") != -1 || exec.getOutput().indexOf("1.6") != -1 : exec + "\n" - + ssh.exec("cat /tmp/bootstrap/stdout.log /tmp/bootstrap/stderr.log"); + + ssh.exec("cat /tmp/bootstrap/stdout.log /tmp/bootstrap/stderr.log"); } finally { if (ssh != null) ssh.disconnect(); diff --git a/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java b/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java index eb82dcf477..037e3b1bac 100644 --- a/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java +++ b/compute/src/test/java/org/jclouds/compute/StubComputeServiceIntegrationTest.java @@ -28,6 +28,7 @@ import static org.testng.Assert.assertEquals; import java.io.FileNotFoundException; import java.io.IOException; import java.io.Serializable; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; @@ -43,11 +44,16 @@ import org.jclouds.net.IPSocket; import org.jclouds.predicates.RetryablePredicate; import org.jclouds.predicates.SocketOpen; import org.jclouds.rest.RestContext; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.jclouds.scriptbuilder.statements.login.AdminAccess.Configuration; import org.jclouds.ssh.SshClient; import org.jclouds.ssh.SshException; import org.jclouds.util.Strings2; import org.testng.annotations.Test; +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.inject.AbstractModule; @@ -97,6 +103,35 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes @Override protected void configure() { + bind(AdminAccess.Configuration.class).toInstance(new Configuration() { + + @Override + public Supplier defaultAdminUsername() { + return Suppliers.ofInstance("defaultAdminUsername"); + } + + @Override + public Supplier> defaultAdminSshKeys() { + return Suppliers.> ofInstance(ImmutableMap.of("public", "publicKey", "private", + "privateKey")); + } + + @Override + public Function cryptFunction() { + return new Function() { + + @Override + public String apply(String input) { + return String.format("crypt(%s)", input); + } + + }; + } + + public Supplier passwordGenerator() { + return Suppliers.ofInstance("randompassword"); + } + }); SshClient.Factory factory = createMock(SshClient.Factory.class); SshClient client1 = createMock(SshClient.class); SshClient client2 = createMock(SshClient.class); @@ -105,14 +140,14 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes SshClient client5 = createMock(SshClient.class); expect(factory.create(new IPSocket("144.175.1.1", 22), new Credentials("root", "password1"))).andReturn( - client1); + client1); runScriptAndService(client1, 1); expect(factory.create(new IPSocket("144.175.1.2", 22), new Credentials("root", "password2"))).andReturn( - client2).times(3); + client2).times(3); expect(factory.create(new IPSocket("144.175.1.2", 22), new Credentials("root", "romeo"))).andThrow( - new SshException("Auth fail")); - + new SshException("Auth fail")); + // run script without backgrounding client2.connect(); expect(client2.exec("echo hello\n")).andReturn(new ExecResponse("hello\n", "", 0)); @@ -121,39 +156,39 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes client2.connect(); try { runScript(client2, "runScriptWithCreds", - Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/runscript.sh")), 2); + Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class + .getResourceAsStream("/runscript.sh")), 2); } catch (IOException e) { Throwables.propagate(e); } client2.disconnect(); expect(factory.create(new IPSocket("144.175.1.3", 22), new Credentials("root", "password3"))).andReturn( - client3).times(2); + client3).times(2); expect(factory.create(new IPSocket("144.175.1.4", 22), new Credentials("root", "password4"))).andReturn( - client4).times(2); + client4).times(2); expect(factory.create(new IPSocket("144.175.1.5", 22), new Credentials("root", "password5"))).andReturn( - client5).times(2); + client5).times(2); runScriptAndInstallSsh(client3, "bootstrap", 3); runScriptAndInstallSsh(client4, "bootstrap", 4); runScriptAndInstallSsh(client5, "bootstrap", 5); expect( - factory.create(eq(new IPSocket("144.175.1.1", 22)), eq(new Credentials("root", keyPair - .get("private"))))).andReturn(client1); + factory.create(eq(new IPSocket("144.175.1.1", 22)), + eq(new Credentials("defaultAdminUsername", "privateKey")))).andReturn(client1); expect( - factory.create(eq(new IPSocket("144.175.1.2", 22)), eq(new Credentials("root", keyPair - .get("private"))))).andReturn(client2); + factory.create(eq(new IPSocket("144.175.1.2", 22)), + eq(new Credentials("defaultAdminUsername", "privateKey")))).andReturn(client2); expect( - factory.create(eq(new IPSocket("144.175.1.3", 22)), eq(new Credentials("root", keyPair - .get("private"))))).andReturn(client3); + factory.create(eq(new IPSocket("144.175.1.3", 22)), + eq(new Credentials("defaultAdminUsername", "privateKey")))).andReturn(client3); expect( - factory.create(eq(new IPSocket("144.175.1.4", 22)), eq(new Credentials("root", keyPair - .get("private"))))).andReturn(client4); + factory.create(eq(new IPSocket("144.175.1.4", 22)), + eq(new Credentials("defaultAdminUsername", "privateKey")))).andReturn(client4); expect( - factory.create(eq(new IPSocket("144.175.1.5", 22)), eq(new Credentials("root", keyPair - .get("private"))))).andReturn(client5); + factory.create(eq(new IPSocket("144.175.1.5", 22)), + eq(new Credentials("defaultAdminUsername", "privateKey")))).andReturn(client5); helloAndJava(client2); helloAndJava(client3); @@ -175,7 +210,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes try { runScript(client, "jboss", Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/initscript_with_jboss.sh")), nodeId); + .getResourceAsStream("/initscript_with_jboss.sh")), nodeId); } catch (IOException e) { Throwables.propagate(e); } @@ -189,7 +224,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes try { runScript(client, scriptName, Strings2.toStringAndClose(StubComputeServiceIntegrationTest.class - .getResourceAsStream("/initscript_with_java.sh")), nodeId); + .getResourceAsStream("/initscript_with_java.sh")), nodeId); } catch (IOException e) { Throwables.propagate(e); } @@ -242,7 +277,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes public void testAssignability() throws Exception { @SuppressWarnings("unused") RestContext, ConcurrentMap> stubContext = new ComputeServiceContextFactory() - .createContext(provider, identity, credential).getProviderSpecificContext(); + .createContext(provider, identity, credential).getProviderSpecificContext(); } private static class PayloadEquals implements IArgumentMatcher, Serializable { @@ -289,7 +324,7 @@ public class StubComputeServiceIntegrationTest extends BaseComputeServiceLiveTes return false; PayloadEquals other = (PayloadEquals) o; return this.expected == null && other.expected == null || this.expected != null - && this.expected.equals(other.expected); + && this.expected.equals(other.expected); } @Override diff --git a/compute/src/test/java/org/jclouds/compute/config/PersistNodeCredentialsTest.java b/compute/src/test/java/org/jclouds/compute/config/PersistNodeCredentialsTest.java new file mode 100644 index 0000000000..afb0c464ca --- /dev/null +++ b/compute/src/test/java/org/jclouds/compute/config/PersistNodeCredentialsTest.java @@ -0,0 +1,147 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.compute.config; + +import static org.easymock.EasyMock.expect; +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; +import static org.easymock.classextension.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import java.util.Map; + +import org.jclouds.compute.config.PersistNodeCredentialsModule.RefreshCredentialsForNode; +import org.jclouds.compute.config.PersistNodeCredentialsModule.RefreshCredentialsForNodeIfRanAdminAccess; +import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.NodeMetadataBuilder; +import org.jclouds.compute.domain.NodeState; +import org.jclouds.compute.internal.PersistNodeCredentials; +import org.jclouds.domain.Credentials; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.TypeLiteral; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class PersistNodeCredentialsTest { + + public void testReturnsCorrectFunction() { + PersistNodeCredentials persistNodeCredentials = Guice.createInjector(new PersistNodeCredentialsModule(), + new AbstractModule() { + + @Override + protected void configure() { + bind(new TypeLiteral>() { + }).toInstance(ImmutableMap. of()); + } + + }).getInstance(PersistNodeCredentials.class); + assertEquals(persistNodeCredentials.always(null).getClass(), + PersistNodeCredentialsModule.RefreshCredentialsForNode.class); + assertEquals(persistNodeCredentials.ifAdminAccess(null).getClass(), + PersistNodeCredentialsModule.RefreshCredentialsForNodeIfRanAdminAccess.class); + } + + public void testRefreshCredentialsForNodeIfRanAdminAccessWhenStatementIsNullSameCredentialsAndNoCaching() { + @SuppressWarnings("unchecked") + Map credstore = createMock(Map.class); + + replay(credstore); + + NodeMetadata node = new NodeMetadataBuilder().ids("id").state(NodeState.RUNNING).build(); + RefreshCredentialsForNodeIfRanAdminAccess fn = new PersistNodeCredentialsModule.RefreshCredentialsForNodeIfRanAdminAccess( + credstore, null); + assertEquals(node, fn.apply(node)); + + verify(credstore); + + } + + public void testRefreshCredentialsForNodeWhenStatementIsNullSameCredentialsAndDoesCache() { + @SuppressWarnings("unchecked") + Map credstore = createMock(Map.class); + Credentials credentials = createMock(Credentials.class); + + expect(credstore.put("node#id", credentials)).andReturn(null); + + replay(credstore); + + NodeMetadata node = new NodeMetadataBuilder().ids("id").state(NodeState.RUNNING).credentials(credentials).build(); + RefreshCredentialsForNode fn = new PersistNodeCredentialsModule.RefreshCredentialsForNode(credstore, null); + assertEquals(node, fn.apply(node)); + + verify(credstore); + + } + + public void testRefreshCredentialsForNodeIfRanAdminAccessWhenStatementIsAdminAccessNewCredentialsAndDoesCache() { + @SuppressWarnings("unchecked") + Map credstore = createMock(Map.class); + + AdminAccess statement = createMock(AdminAccess.class); + Credentials credentials = createMock(Credentials.class); + + expect(statement.getAdminCredentials()).andReturn(credentials).atLeastOnce(); + expect(credstore.put("node#id", credentials)).andReturn(null); + + replay(statement); + replay(credstore); + + NodeMetadata node = new NodeMetadataBuilder().ids("id").state(NodeState.RUNNING).build(); + RefreshCredentialsForNodeIfRanAdminAccess fn = new PersistNodeCredentialsModule.RefreshCredentialsForNodeIfRanAdminAccess( + credstore, statement); + assertEquals(fn.apply(node).getCredentials(), credentials); + + verify(statement); + verify(credstore); + + } + + public void testRefreshCredentialsForNodeWhenStatementIsAdminAccessNewCredentialsAndDoesCache() { + @SuppressWarnings("unchecked") + Map credstore = createMock(Map.class); + + AdminAccess statement = createMock(AdminAccess.class); + Credentials credentials = createMock(Credentials.class); + expect(statement.getAdminCredentials()).andReturn(credentials).atLeastOnce(); + expect(credstore.put("node#id", credentials)).andReturn(null); + expect(credstore.put("node#id", credentials)).andReturn(null); // TODO + // optimize + // this + + replay(statement); + replay(credstore); + + NodeMetadata node = new NodeMetadataBuilder().ids("id").state(NodeState.RUNNING).build(); + RefreshCredentialsForNode fn = new PersistNodeCredentialsModule.RefreshCredentialsForNode(credstore, statement); + assertEquals(fn.apply(node).getCredentials(), credentials); + + verify(statement); + verify(credstore); + + } + +} diff --git a/compute/src/test/resources/initscript_with_java.sh b/compute/src/test/resources/initscript_with_java.sh index a3a361c7dc..68db70f4ef 100644 --- a/compute/src/test/resources/initscript_with_java.sh +++ b/compute/src/test/resources/initscript_with_java.sh @@ -76,23 +76,33 @@ END_OF_SCRIPT # add desired commands from the user cat >> $INSTANCE_HOME/bootstrap.sh <<'END_OF_SCRIPT' cd $INSTANCE_HOME -mkdir -p ~/.ssh -cat >> ~/.ssh/authorized_keys <<'END_OF_FILE' -ssh-rsa +rm /etc/sudoers +cat >> /etc/sudoers <<'END_OF_FILE' +root ALL = (ALL) ALL +%wheel ALL = (ALL) NOPASSWD:ALL END_OF_FILE -chmod 600 ~/.ssh/authorized_keys +chmod 0440 /etc/sudoers +mkdir -p /home/users/defaultAdminUsername +groupadd -f wheel +useradd -s /bin/bash -g wheel -d /home/users/defaultAdminUsername -p 'crypt(randompassword)' defaultAdminUsername +mkdir -p /home/users/defaultAdminUsername/.ssh +cat >> /home/users/defaultAdminUsername/.ssh/authorized_keys <<'END_OF_FILE' +publicKey +END_OF_FILE +chmod 600 /home/users/defaultAdminUsername/.ssh/authorized_keys +chown -R defaultAdminUsername /home/users/defaultAdminUsername +exec 3<> /etc/ssh/sshd_config && awk -v TEXT="PasswordAuthentication no +PermitRootLogin no +" 'BEGIN {print TEXT}{print}' /etc/ssh/sshd_config >&3 +/etc/init.d/sshd reload||/etc/init.d/ssh reload +awk -v user=^${SUDO_USER:=${USER}}: -v password='crypt(randompassword)' 'BEGIN { FS=OFS=":" } $0 ~ user { $2 = password } 1' /etc/shadow >/etc/shadow.${SUDO_USER:=${USER}} +test -f /etc/shadow.${SUDO_USER:=${USER}} && mv /etc/shadow.${SUDO_USER:=${USER}} /etc/shadow which curl || (apt-get install -f -y -qq --force-yes curl || (apt-get update && apt-get install -f -y -qq --force-yes curl)) (which java && java -fullversion 2>&1|egrep -q 1.6 ) || curl -X GET -s --retry 20 http://whirr.s3.amazonaws.com/0.2.0-incubating-SNAPSHOT/sun/java/install |(bash) echo nameserver 208.67.222.222 >> /etc/resolv.conf rm -rf /var/cache/apt /usr/lib/vmware-tools echo "export PATH=\"\$JAVA_HOME/bin/:\$PATH\"" >> /root/.bashrc -mkdir -p ~/.ssh -rm ~/.ssh/id_rsa -cat >> ~/.ssh/id_rsa <<'END_OF_FILE' ------BEGIN RSA PRIVATE KEY----- -END_OF_FILE -chmod 600 ~/.ssh/id_rsa END_OF_SCRIPT diff --git a/core/src/main/java/org/jclouds/crypto/Crypto.java b/core/src/main/java/org/jclouds/crypto/Crypto.java index 187470f7f0..fb54f1cee3 100644 --- a/core/src/main/java/org/jclouds/crypto/Crypto.java +++ b/core/src/main/java/org/jclouds/crypto/Crypto.java @@ -20,6 +20,7 @@ package org.jclouds.crypto; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateFactory; @@ -31,13 +32,14 @@ import org.jclouds.encryption.internal.JCECrypto; import com.google.inject.ImplementedBy; /** - * Allows you to access cryptographic objects and factories without adding a provider to the JCE - * runtime. + * Allows you to access cryptographic objects and factories without adding a + * provider to the JCE runtime. * * @author Adrian Cole */ @ImplementedBy(JCECrypto.class) public interface Crypto { + KeyPairGenerator rsaKeyPairGenerator(); KeyFactory rsaKeyFactory(); diff --git a/core/src/main/java/org/jclouds/crypto/Pems.java b/core/src/main/java/org/jclouds/crypto/Pems.java index 02460e7657..e965d12030 100644 --- a/core/src/main/java/org/jclouds/crypto/Pems.java +++ b/core/src/main/java/org/jclouds/crypto/Pems.java @@ -93,8 +93,8 @@ public class Pems { if (parsers.containsKey(reader.getBeginMarker())) { return parsers.get(reader.getBeginMarker()).parseResult(bytes); } else { - throw new IOException(String.format("Invalid PEM file: no parsers for marker %s in %s", reader - .getBeginMarker(), parsers.keySet())); + throw new IOException(String.format("Invalid PEM file: no parsers for marker %s in %s", + reader.getBeginMarker(), parsers.keySet())); } } catch (IOException e) { throw new RuntimeException(e); @@ -103,7 +103,8 @@ public class Pems { } /** - * Returns the object of generic type {@code T} that is pem encoded in the supplier. + * Returns the object of generic type {@code T} that is pem encoded in the + * supplier. * * @param supplier * the input stream factory @@ -111,12 +112,13 @@ public class Pems { * header that begins the PEM block * @param processor * how to parser the object from a byte array - * @return the object of generic type {@code T} which was PEM encoded in the stream + * @return the object of generic type {@code T} which was PEM encoded in the + * stream * @throws IOException * if an I/O error occurs */ public static T fromPem(InputSupplier supplier, PemProcessor processor) - throws IOException { + throws IOException { try { return com.google.common.io.ByteStreams.readBytes(supplier, processor); } catch (RuntimeException e) { @@ -138,25 +140,27 @@ public class Pems { * if an I/O error occurs */ public static KeySpec privateKeySpec(InputSupplier supplier) throws IOException { - return fromPem(supplier, new PemProcessor(ImmutableMap.> of( - PRIVATE_PKCS1_MARKER, new ResultParser() { + return fromPem( + supplier, + new PemProcessor(ImmutableMap.> of(PRIVATE_PKCS1_MARKER, + new ResultParser() { - public KeySpec parseResult(byte[] bytes) throws IOException { - return (new PKCS1EncodedKeySpec(bytes)).getKeySpec(); - } + public KeySpec parseResult(byte[] bytes) throws IOException { + return (new PKCS1EncodedKeySpec(bytes)).getKeySpec(); + } - }, PRIVATE_PKCS8_MARKER, new ResultParser() { + }, PRIVATE_PKCS8_MARKER, new ResultParser() { - public KeySpec parseResult(byte[] bytes) throws IOException { - return new PKCS8EncodedKeySpec(bytes); - } + public KeySpec parseResult(byte[] bytes) throws IOException { + return new PKCS8EncodedKeySpec(bytes); + } - }))); + }))); } /** - * Executes {@link Pems#privateKeySpec(InputSupplier)} on the string which contains an encoded - * private key in PEM format. + * Executes {@link Pems#privateKeySpec(InputSupplier)} on the string which + * contains an encoded private key in PEM format. * * @param pem * private key in pem encoded format. @@ -177,25 +181,27 @@ public class Pems { * if an I/O error occurs */ public static KeySpec publicKeySpec(InputSupplier supplier) throws IOException { - return fromPem(supplier, new PemProcessor(ImmutableMap.> of( - PUBLIC_PKCS1_MARKER, new ResultParser() { + return fromPem( + supplier, + new PemProcessor(ImmutableMap.> of(PUBLIC_PKCS1_MARKER, + new ResultParser() { - public KeySpec parseResult(byte[] bytes) throws IOException { - return (new PKCS1EncodedPublicKeySpec(bytes)).getKeySpec(); - } + public KeySpec parseResult(byte[] bytes) throws IOException { + return (new PKCS1EncodedPublicKeySpec(bytes)).getKeySpec(); + } - }, PUBLIC_X509_MARKER, new ResultParser() { + }, PUBLIC_X509_MARKER, new ResultParser() { - public X509EncodedKeySpec parseResult(byte[] bytes) throws IOException { - return new X509EncodedKeySpec(bytes); - } + public X509EncodedKeySpec parseResult(byte[] bytes) throws IOException { + return new X509EncodedKeySpec(bytes); + } - }))); + }))); } /** - * Executes {@link Pems#publicKeySpec(InputSupplier)} on the string which contains an encoded - * public key in PEM format. + * Executes {@link Pems#publicKeySpec(InputSupplier)} on the string which + * contains an encoded public key in PEM format. * * @param pem * public key in pem encoded format. @@ -206,7 +212,8 @@ public class Pems { } /** - * Returns the {@link X509EncodedKeySpec} that is pem encoded in the supplier. + * Returns the {@link X509EncodedKeySpec} that is pem encoded in the + * supplier. * * @param supplier * the input stream factory @@ -219,24 +226,25 @@ public class Pems { * @throws CertificateException */ public static X509Certificate x509Certificate(InputSupplier supplier, - @Nullable CertificateFactory certFactory) throws IOException, CertificateException { + @Nullable CertificateFactory certFactory) throws IOException, CertificateException { final CertificateFactory finalCertFactory = certFactory != null ? certFactory : CertificateFactory - .getInstance("X.509"); + .getInstance("X.509"); try { - return fromPem(supplier, new PemProcessor(ImmutableMap - .> of(CERTIFICATE_X509_MARKER, - new ResultParser() { + return fromPem( + supplier, + new PemProcessor(ImmutableMap.> of( + CERTIFICATE_X509_MARKER, new ResultParser() { - public X509Certificate parseResult(byte[] bytes) throws IOException { - try { - return (X509Certificate) finalCertFactory - .generateCertificate(new ByteArrayInputStream(bytes)); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } + public X509Certificate parseResult(byte[] bytes) throws IOException { + try { + return (X509Certificate) finalCertFactory.generateCertificate(new ByteArrayInputStream( + bytes)); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } - }))); + }))); } catch (RuntimeException e) { if (e.getCause() != null && e.getCause() instanceof CertificateException) { throw (CertificateException) e.getCause(); @@ -246,8 +254,8 @@ public class Pems { } /** - * Executes {@link Pems#x509Certificate(InputSupplier, CertificateFactory)} on the string which - * contains an X.509 certificate in PEM format. + * Executes {@link Pems#x509Certificate(InputSupplier, CertificateFactory)} + * on the string which contains an X.509 certificate in PEM format. * * @param pem * certificate in pem encoded format. @@ -297,14 +305,14 @@ public class Pems { public static String pem(PrivateKey key) { String marker = key instanceof RSAPrivateCrtKey ? PRIVATE_PKCS1_MARKER : PRIVATE_PKCS8_MARKER; return pem(key instanceof RSAPrivateCrtKey ? getEncoded(RSAPrivateCrtKey.class.cast(key)) : key.getEncoded(), - marker); + marker); } // TODO find a way to do this without using bouncycastle static byte[] getEncoded(RSAPrivateCrtKey key) { - RSAPrivateKeyStructure keyStruct = new RSAPrivateKeyStructure(key.getModulus(), key.getPublicExponent(), key - .getPrivateExponent(), key.getPrimeP(), key.getPrimeQ(), key.getPrimeExponentP(), key - .getPrimeExponentQ(), key.getCrtCoefficient()); + RSAPrivateKeyStructure keyStruct = new RSAPrivateKeyStructure(key.getModulus(), key.getPublicExponent(), + key.getPrivateExponent(), key.getPrimeP(), key.getPrimeQ(), key.getPrimeExponentP(), + key.getPrimeExponentQ(), key.getCrtCoefficient()); ByteArrayOutputStream bOut = new ByteArrayOutputStream(); ASN1OutputStream aOut = new ASN1OutputStream(bOut); @@ -320,9 +328,13 @@ public class Pems { } private static String pem(byte[] key, String marker) { - return new StringBuilder(marker + "\n").append( - Joiner.on('\n').join(Splitter.fixedLength(64).split(CryptoStreams.base64(key)))).append( - "\n" + marker.replace("BEGIN", "END") + "\n").toString().trim(); + return pem(key, marker, 64); + } + + static String pem(byte[] key, String marker, int length) { + return new StringBuilder(marker + "\n") + .append(Joiner.on('\n').join(Splitter.fixedLength(length).split(CryptoStreams.base64(key)))) + .append("\n" + marker.replace("BEGIN", "END") + "\n").toString().trim(); } } diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Sha512Crypt.java b/core/src/main/java/org/jclouds/crypto/Sha512Crypt.java similarity index 86% rename from scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Sha512Crypt.java rename to core/src/main/java/org/jclouds/crypto/Sha512Crypt.java index b879ad38a8..c352b72c3e 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/util/Sha512Crypt.java +++ b/core/src/main/java/org/jclouds/crypto/Sha512Crypt.java @@ -44,28 +44,32 @@ */ -package org.jclouds.scriptbuilder.util; +package org.jclouds.crypto; import java.security.MessageDigest; import javax.annotation.Nullable; -import org.jclouds.crypto.Crypto; +import org.jclouds.encryption.internal.JCECrypto; + +import com.google.common.base.Throwables; /** * This class defines a method, - * {@link Sha512Crypt#Sha512_crypt(java.lang.String, java.lang.String, int) Sha512_crypt()}, which - * takes a password and a salt string and generates a Sha512 encrypted password entry. + * {@link Sha512Crypt#Sha512_crypt(java.lang.String, java.lang.String, int) + * Sha512_crypt()}, which takes a password and a salt string and generates a + * Sha512 encrypted password entry. * - * This class implements the new generation, scalable, SHA512-based Unix 'crypt' algorithm developed - * by a group of engineers from Red Hat, Sun, IBM, and HP for common use in the Unix and Linux - * /etc/shadow files. + * This class implements the new generation, scalable, SHA512-based Unix 'crypt' + * algorithm developed by a group of engineers from Red Hat, Sun, IBM, and HP + * for common use in the Unix and Linux /etc/shadow files. * - * The Linux glibc library (starting at version 2.7) includes support for validating passwords - * hashed using this algorithm. + * The Linux glibc library (starting at version 2.7) includes support for + * validating passwords hashed using this algorithm. * * The algorithm itself was released into the Public Domain by Ulrich Drepper - * <drepper@redhat.com>. A discussion of the rationale and development of this algorithm is at + * <drepper@redhat.com>. A discussion of the rationale and development of + * this algorithm is at * * http://people.redhat.com/drepper/sha-crypt.html * @@ -74,6 +78,34 @@ import org.jclouds.crypto.Crypto; * http://people.redhat.com/drepper/SHA-crypt.txt */ public class Sha512Crypt { + public static com.google.common.base.Function function() { + return Function.INSTANCE; + } + + public static enum Function implements com.google.common.base.Function { + INSTANCE; + private Crypto crypto; + + Function() { + try { + this.crypto = new JCECrypto(); + } catch (Exception e) { + Throwables.propagate(e); + } + } + + @Override + public String apply(String input) { + return Sha512Crypt.makeShadowLine(input, null, crypto); + } + + @Override + public String toString() { + return "sha512Crypt()"; + } + + } + static private final String sha512_salt_prefix = "$6$"; static private final String sha512_rounds_prefix = "rounds="; static private final int SALT_LEN_MAX = 16; @@ -84,19 +116,20 @@ public class Sha512Crypt { static private final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /** - * This method actually generates an Sha512 crypted password hash from a plaintext password and a - * salt. + * This method actually generates an Sha512 crypted password hash from a + * plaintext password and a salt. * *

- * The resulting string will be in the form '$6$<rounds=n>$<salt>$<hashed mess> + * The resulting string will be in the form + * '$6$<rounds=n>$<salt>$<hashed mess> *

* * @param password * Plaintext password * * @param shadowPrefix - * An encoded salt/rounds which will be consulted to determine the salt and round - * count, if not null + * An encoded salt/rounds which will be consulted to determine the + * salt and round count, if not null * * @return The Sha512 Unix Crypt hash text for the password */ @@ -206,7 +239,8 @@ public class Sha512Crypt { System.arraycopy(temp_result, 0, s_bytes, cnt2, cnt); /* - * Repeatedly run the collected hash value through SHA512 to burn CPU cycles. + * Repeatedly run the collected hash value through SHA512 to burn CPU + * cycles. */ for (cnt = 0; cnt < rounds; ++cnt) { @@ -270,8 +304,8 @@ public class Sha512Crypt { buffer.append(b64_from_24bit((byte) 0x00, (byte) 0x00, alt_result[63], 2)); /* - * Clear the buffer for the intermediate result so that people attaching to processes or - * reading core dumps cannot get any information. + * Clear the buffer for the intermediate result so that people attaching + * to processes or reading core dumps cannot get any information. */ ctx.reset(); diff --git a/core/src/main/java/org/jclouds/crypto/SshKeys.java b/core/src/main/java/org/jclouds/crypto/SshKeys.java new file mode 100644 index 0000000000..b9c1768eca --- /dev/null +++ b/core/src/main/java/org/jclouds/crypto/SshKeys.java @@ -0,0 +1,111 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.crypto; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Map; + +import com.google.common.annotations.Beta; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +/** + * Creates OpenSSH RSA keypairs + * + * @author Adrian Cole + * @see + */ +@Beta +public class SshKeys { + // All data type encoding is defined in the section #5 of RFC #4251. string + // and mpint (multiple precision integer) types are encoded this way : + private static final byte[] sshrsa = new byte[] { 0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a' }; + + /** + * + * @param used + * to generate RSA key pairs + * @return new 2048 bit keyPair + * @see Crypto#rsaKeyPairGenerator() + */ + public static KeyPair generateRsaKeyPair(KeyPairGenerator generator) { + generator.initialize(2048); + return generator.genKeyPair(); + } + + /** + * return a "public" -> rsa public key, "private" -> its corresponding + * private key + */ + public static Map generate() { + try { + return generate(KeyPairGenerator.getInstance("RSA")); + } catch (NoSuchAlgorithmException e) { + Throwables.propagate(e); + return null; + } + } + + public static Map generate(KeyPairGenerator generator) { + KeyPair pair = generateRsaKeyPair(generator); + Builder builder = ImmutableMap. builder(); + builder.put("public", encodeAsOpenSSH(RSAPublicKey.class.cast(pair.getPublic()))); + builder.put("private", encodeAsPem(RSAPrivateKey.class.cast(pair.getPrivate()))); + return builder.build(); + } + + public static String encodeAsOpenSSH(RSAPublicKey key) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + /* encode the "ssh-rsa" string */ + out.write(sshrsa); + /* Encode the public exponent */ + BigInteger e = key.getPublicExponent(); + byte[] data = e.toByteArray(); + encodeUint32(data.length, out); + out.write(data); + /* Encode the modulus */ + BigInteger m = key.getModulus(); + data = m.toByteArray(); + encodeUint32(data.length, out); + out.write(data); + return "ssh-rsa " + CryptoStreams.base64(out.toByteArray()); + } + + public static String encodeAsPem(RSAPrivateKey key) { + return Pems.pem(key.getEncoded(), Pems.PRIVATE_PKCS1_MARKER, 64); + } + + public static void encodeUint32(int value, ByteArrayDataOutput out) { + out.write((byte) ((value >>> 24) & 0xff)); + out.write((byte) ((value >>> 16) & 0xff)); + out.write((byte) ((value >>> 8) & 0xff)); + out.write((byte) (value & 0xff)); + } + +} diff --git a/core/src/main/java/org/jclouds/encryption/internal/JCECrypto.java b/core/src/main/java/org/jclouds/encryption/internal/JCECrypto.java index 3afbc2dd5f..1363e65c5b 100755 --- a/core/src/main/java/org/jclouds/encryption/internal/JCECrypto.java +++ b/core/src/main/java/org/jclouds/encryption/internal/JCECrypto.java @@ -20,6 +20,7 @@ package org.jclouds.encryption.internal; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Provider; @@ -41,6 +42,7 @@ import org.jclouds.crypto.Crypto; @Singleton public class JCECrypto implements Crypto { + private final KeyPairGenerator rsaKeyPairGenerator; private final KeyFactory rsaKeyFactory; private final CertificateFactory certFactory; private final Provider provider; @@ -51,9 +53,11 @@ public class JCECrypto implements Crypto { } public JCECrypto(@Nullable Provider provider) throws NoSuchAlgorithmException, CertificateException { + this.rsaKeyPairGenerator = provider == null ? KeyPairGenerator.getInstance("RSA") : KeyPairGenerator.getInstance( + "RSA", provider); this.rsaKeyFactory = provider == null ? KeyFactory.getInstance("RSA") : KeyFactory.getInstance("RSA", provider); this.certFactory = provider == null ? CertificateFactory.getInstance("X.509") : CertificateFactory.getInstance( - "X.509", provider); + "X.509", provider); this.provider = provider; } @@ -142,4 +146,9 @@ public class JCECrypto implements Crypto { public KeyFactory rsaKeyFactory() { return rsaKeyFactory; } + + @Override + public KeyPairGenerator rsaKeyPairGenerator() { + return rsaKeyPairGenerator; + } } diff --git a/core/src/main/java/org/jclouds/util/PasswordGenerator.java b/core/src/main/java/org/jclouds/util/PasswordGenerator.java new file mode 100644 index 0000000000..7a632b1b93 --- /dev/null +++ b/core/src/main/java/org/jclouds/util/PasswordGenerator.java @@ -0,0 +1,60 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.util; + +import java.security.SecureRandom; + +import com.google.common.base.Supplier; + +/** + * Cheap, lightweight, low-security password generator. + * + * @see + */ +public enum PasswordGenerator implements Supplier { + + INSTANCE; + + /** Minimum length for a decent password */ + public static final int MIN_LENGTH = 10; + + /** The random number generator. */ + protected static final SecureRandom r = new SecureRandom(); + + /* + * Set of characters that is valid. Must be printable, memorable, and + * "won't break HTML" (i.e., not ' <', '>', '&', '=', ...). or break shell + * commands (i.e., not ' <', '>', '$', '!', ...). I, L and O are good to + * leave out, as are numeric zero and one. + */ + public final static char[] goodChar = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '@', }; + + @Override + public String get() { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < MIN_LENGTH; i++) { + sb.append(goodChar[r.nextInt(goodChar.length)]); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/util/Sha512CryptTest.java b/core/src/test/java/org/jclouds/crypto/Sha512CryptTest.java similarity index 97% rename from scriptbuilder/src/test/java/org/jclouds/scriptbuilder/util/Sha512CryptTest.java rename to core/src/test/java/org/jclouds/crypto/Sha512CryptTest.java index 1b7d52e4d2..07dea0c86a 100644 --- a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/util/Sha512CryptTest.java +++ b/core/src/test/java/org/jclouds/crypto/Sha512CryptTest.java @@ -16,11 +16,10 @@ * limitations under the License. * ==================================================================== */ -package org.jclouds.scriptbuilder.util; +package org.jclouds.crypto; import static org.testng.Assert.assertEquals; -import org.jclouds.crypto.Crypto; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; diff --git a/core/src/test/java/org/jclouds/crypto/SshKeysTest.java b/core/src/test/java/org/jclouds/crypto/SshKeysTest.java new file mode 100644 index 0000000000..fc9487d51d --- /dev/null +++ b/core/src/test/java/org/jclouds/crypto/SshKeysTest.java @@ -0,0 +1,65 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.crypto; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Map; + +import org.jclouds.io.Payloads; +import org.testng.annotations.Test; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit", singleThreaded = true) +public class SshKeysTest { + + public static final String SSH_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJvZkkmoabQopH7yd9Ak2xJ34X21c0xXsJ85xbqOyqzwRmCJVHT2EPUhg6PhiozSok42WDKDjFFZ7B1IbtBM+PWUmlUCJ1r2xfLb6TPJqBkDUCbQ5lxupvmGh4gOBxf54Nrv2zS7QOiaNx870QqG8csHP7Mz7dCo9GQ9XydhNt+z4eNXPM5ToPUHRdHf4g9lmXYCdazZ3SqGdK0dwNS+dFVDQ/jzZjA31WBx45m2k/PQMIoQnlPHlJO5vyTT/O39UAwdBp8tK5Awt3azhku45mm03/+4CxObGKt6p3fvP7xlN0FsnwsSkn6IJe5J+blpSHuLDqjG9OhjYAvnf6MrZR"; + public static final String SSH_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDJvZkkmoabQopH\\n7yd9Ak2xJ34X21c0xXsJ85xbqOyqzwRmCJVHT2EPUhg6PhiozSok42WDKDjFFZ7B\\n1IbtBM+PWUmlUCJ1r2xfLb6TPJqBkDUCbQ5lxupvmGh4gOBxf54Nrv2zS7QOiaNx\\n870QqG8csHP7Mz7dCo9GQ9XydhNt+z4eNXPM5ToPUHRdHf4g9lmXYCdazZ3SqGdK\\n0dwNS+dFVDQ/jzZjA31WBx45m2k/PQMIoQnlPHlJO5vyTT/O39UAwdBp8tK5Awt3\\nazhku45mm03/+4CxObGKt6p3fvP7xlN0FsnwsSkn6IJe5J+blpSHuLDqjG9OhjYA\\nvnf6MrZRAgMBAAECggEBAMDzwHeL/FafS9cFXEVqYJih5y42MbBdeRLJl7DrXoD4\\nQ4K7jtuHhpO6t0VtgvRgVoC1pa/OVo3Z4eANv4cO5N58Tb35aRwaTpKyE+aLPlPR\\nc4IAgJbDrBJUOQeYbBLiNm9sAWbtbyfAaT1iHGDEWJGeCzAlkWik4ugXlZeza13y\\nC4hg2yUymju27nSIWcEfAo7Zmw8CTy32edLGUNq8qu7KYSaocKvf522Kjl5p6cly\\nnd+OhcFA4pSTeKkgjb1dEHw2p4JVX/srHCpySo4ERtHAFyVyTIhsP5GICAq856U5\\nGMSIT1Hja+6q/FEDqT2R32ju62VfQBS2kSfeBlzot6kCgYEA/1p6Xljvn6wrYklQ\\n046E7xp7jlx2yKWnbIjRf5QFpK5Czb53I+mSkQcDF3imaZK+f/xz1OURmGv6phsA\\nFdU+UOdzGki7DYCNuTuz9LhdpIqxGqlcvnvOP9A6ouRGTz45VX2rqUx5A2ou0SPW\\n7ok++JfvRGj4VC1G2005ht90/98CgYEAykBeNHnChXmxRv5ESD28S5ijxFLI5qvG\\nooUIhdVQ2ws7H7bb4Arj4JClu55Y3uOi2uVgPq5Pqy9aLcAALuSJjRQj9+NU9EJS\\nLy1WhBQTRNpTlUshNpg4H5b9sFoMTgz3nrTU24NY73RtoDl19TjiK7O9rTasYlcr\\n36dLejyeT88CgYEAs4vp2OcN7ibABobolyhx3jGvyOTI/MJFm7IEJIFvCmEhRctz\\nuEOms+TLTridwkPVQObAh2Rd39+kySDZCYD8JSTosQWMyKyoeiM5oIv2BBkk+Es3\\nlBQ3bHU8lYaOzW9CHxOTHSJRQI5rxtA9c1H7fg5OxbpNSdrgJJkDJwt+F98CgYEA\\niCBWx58EK+5CQXQ15SGYMJFl+Gd3zLnlEdHUcK+ooiWm/6uFxf/ObIEu616iljJE\\nlGw6ITYVbTSLz6sg9G7hndDmfJvHvDc/NX2gc3lHltoT07IjgqllbO2lhiK1kXrs\\n1ycC9VQscc69UlAacph8scliaskXsYDWiMwC4x0VuMUCgYA/FSHHAXLyBqoFqEpQ\\nRmfgXzUcU/mKEaGRPhAvoAU87Z74mEgaO5Hbv7zmFwWpD4cdW0WybzVv9r5Hs/hS\\n4JGBzx4KPFAygnVeXiftVuM9scycg3RHK907hmlYNZicI7TbUlM5RQ4ngn/LTjXo\\nG+TDTPn3Luw1fvAMEEcdsr9cJw==\\n-----END RSA PRIVATE KEY-----"; + + @Test + public void testCanGenerate() { + Map map = SshKeys.generate(); + assert map.get("public").startsWith("ssh-rsa ") : map; + assert map.get("private").startsWith("-----BEGIN RSA PRIVATE KEY-----") : map; + } + + @Test + public void testEncodeAsPem() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + String encoded = SshKeys.encodeAsPem((RSAPrivateCrtKey) KeyFactory.getInstance("RSA").generatePrivate( + Pems.privateKeySpec(Payloads.newStringPayload(PemsTest.PRIVATE_KEY)))); + assertEquals(encoded.replace("\n", "\\n").trim(), SSH_PRIVATE_KEY); + } + + @Test + public void testEncodeAsOpenSSH() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + String encoded = SshKeys.encodeAsOpenSSH((RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic( + Pems.publicKeySpec(Payloads.newStringPayload(PemsTest.PUBLIC_KEY)))); + assertEquals(encoded, SSH_PUBLIC_KEY); + } + +} diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeService.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeService.java index 8b7bf063c5..63ffece204 100644 --- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeService.java +++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeService.java @@ -40,6 +40,7 @@ import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.TemplateBuilder; +import org.jclouds.compute.internal.PersistNodeCredentials; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.compute.reference.ComputeServiceConstants.Timeouts; import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet; @@ -56,6 +57,7 @@ import org.jclouds.ec2.compute.EC2ComputeService; import org.jclouds.ec2.compute.domain.RegionAndName; import org.jclouds.ec2.compute.options.EC2TemplateOptions; import org.jclouds.ec2.domain.KeyPair; +import org.jclouds.scriptbuilder.functions.InitAdminAccess; import org.jclouds.util.Preconditions2; import com.google.common.annotations.VisibleForTesting; @@ -83,7 +85,8 @@ public class AWSEC2ComputeService extends EC2ComputeService { @Named("NODE_RUNNING") Predicate nodeRunning, @Named("NODE_TERMINATED") Predicate nodeTerminated, @Named("NODE_SUSPENDED") Predicate nodeSuspended, - InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, Timeouts timeouts, + InitializeRunScriptOnNodeOrPlaceInBadMap.Factory initScriptRunnerFactory, InitAdminAccess initAdminAccess, + PersistNodeCredentials persistNodeCredentials, Timeouts timeouts, @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, AWSEC2Client ec2Client, Map credentialsMap, @Named("SECURITY") Map securityGroupMap, @Named("PLACEMENT") Map placementGroupMap, @@ -91,7 +94,8 @@ public class AWSEC2ComputeService extends EC2ComputeService { super(context, credentialStore, images, sizes, locations, listNodesStrategy, getNodeMetadataStrategy, runNodesAndAddToSetStrategy, rebootNodeStrategy, destroyNodeStrategy, startNodeStrategy, stopNodeStrategy, templateBuilderProvider, templateOptionsProvider, nodeRunning, nodeTerminated, nodeSuspended, - initScriptRunnerFactory, timeouts, executor, ec2Client, credentialsMap, securityGroupMap); + initScriptRunnerFactory, initAdminAccess, persistNodeCredentials, timeouts, executor, ec2Client, + credentialsMap, securityGroupMap); this.ec2Client = ec2Client; this.placementGroupMap = placementGroupMap; this.placementGroupDeleted = placementGroupDeleted; diff --git a/providers/slicehost/src/test/java/org/jclouds/slicehost/compute/SlicehostComputeServiceLiveTest.java b/providers/slicehost/src/test/java/org/jclouds/slicehost/compute/SlicehostComputeServiceLiveTest.java index add4ebab51..fc8c04d84b 100644 --- a/providers/slicehost/src/test/java/org/jclouds/slicehost/compute/SlicehostComputeServiceLiveTest.java +++ b/providers/slicehost/src/test/java/org/jclouds/slicehost/compute/SlicehostComputeServiceLiveTest.java @@ -37,7 +37,7 @@ import org.testng.annotations.Test; * * @author Adrian Cole */ -@Test(groups = "live", enabled = true, sequential = true) +@Test(groups = "live", enabled = true, singleThreaded = true) public class SlicehostComputeServiceLiveTest extends BaseComputeServiceLiveTest { public SlicehostComputeServiceLiveTest() { provider = "slicehost"; @@ -49,7 +49,7 @@ public class SlicehostComputeServiceLiveTest extends BaseComputeServiceLiveTest assertEquals(defaultTemplate.getImage().getOperatingSystem().is64Bit(), true); assertEquals(defaultTemplate.getImage().getOperatingSystem().getVersion(), "10.04"); assertEquals(defaultTemplate.getImage().getOperatingSystem().getFamily(), OsFamily.UBUNTU); - assertEquals(defaultTemplate.getLocation().getId(), "DFW1"); + assertEquals(defaultTemplate.getLocation().getId(), "slicehost"); assertEquals(getCores(defaultTemplate.getHardware()), 0.25d); } diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/StatementList.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/StatementList.java index c763239e85..4cdd1d99e8 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/StatementList.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/domain/StatementList.java @@ -22,8 +22,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.util.List; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; /** * Statements used in a shell script @@ -35,11 +35,11 @@ public class StatementList implements Statement { public final List statements; public StatementList(Statement... statements) { - this.statements = Lists.newArrayList(checkNotNull(statements, "statements")); + this.statements = ImmutableList.copyOf(checkNotNull(statements, "statements")); } public StatementList(Iterable statements) { - this.statements = Lists.newArrayList(checkNotNull(statements, "statements")); + this.statements = ImmutableList.copyOf(checkNotNull(statements, "statements")); } public String render(OsFamily family) { @@ -52,10 +52,39 @@ public class StatementList implements Statement { @Override public Iterable functionDependencies(OsFamily family) { - List functions = Lists.newArrayList(); + Builder functions = ImmutableList. builder(); for (Statement statement : statements) { - Iterables.addAll(functions, statement.functionDependencies(family)); + functions.addAll(statement.functionDependencies(family)); } - return functions; + return functions.build(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((statements == null) ? 0 : statements.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + StatementList other = (StatementList) obj; + if (statements == null) { + if (other.statements != null) + return false; + } else if (!statements.equals(other.statements)) + return false; + return true; + } + + public List getStatements() { + return statements; } } \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccess.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccess.java new file mode 100644 index 0000000000..123f4e0ef8 --- /dev/null +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccess.java @@ -0,0 +1,57 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.functions; + +import java.util.NoSuchElementException; + +import javax.annotation.Nullable; + +import org.jclouds.domain.Credentials; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.StatementList; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; + +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; + +/** + * + * @author Adrian Cole + */ +public enum CredentialsFromAdminAccess implements Function { + INSTANCE; + @Override + public Credentials apply(@Nullable Statement input) { + if (input == null) + return null; + if (input instanceof StatementList) { + try { + return apply(Iterables.find(StatementList.class.cast(input).getStatements(), + Predicates.instanceOf(AdminAccess.class))); + } catch (NoSuchElementException e) { + return null; + } + } else if (input instanceof AdminAccess) { + return AdminAccess.class.cast(input).getAdminCredentials(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/InitAdminAccess.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/InitAdminAccess.java new file mode 100644 index 0000000000..f6f8d5c773 --- /dev/null +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/functions/InitAdminAccess.java @@ -0,0 +1,58 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.functions; + +import javax.inject.Inject; + +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.StatementList; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.jclouds.scriptbuilder.statements.login.AdminAccess.Configuration; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +/** + * Statement used in a shell script + * + * @author Adrian Cole + */ +public class InitAdminAccess implements Function { + private final AdminAccess.Configuration adminAccessConfiguration; + + @Inject + public InitAdminAccess(Configuration adminAccessConfiguration) { + this.adminAccessConfiguration = adminAccessConfiguration; + } + + @Override + public Statement apply(Statement input) { + if (input instanceof StatementList) { + Builder statements = ImmutableList. builder(); + for (Statement statement : StatementList.class.cast(input).getStatements()) + statements.add(apply(statement)); + return new StatementList(statements.build()); + } else if (input instanceof AdminAccess) { + return AdminAccess.class.cast(input).apply(adminAccessConfiguration); + } else { + return input; + } + } +} \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/AdminAccess.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/AdminAccess.java new file mode 100644 index 0000000000..e08d0fd3c1 --- /dev/null +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/AdminAccess.java @@ -0,0 +1,295 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.statements.login; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.jclouds.crypto.Sha512Crypt; +import org.jclouds.domain.Credentials; +import org.jclouds.scriptbuilder.domain.OsFamily; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.StatementList; +import org.jclouds.scriptbuilder.statements.login.AdminAccess.Configuration; +import org.jclouds.scriptbuilder.statements.ssh.SshStatements; + +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.google.inject.ImplementedBy; + +/** + * Controls the administrative access to a node. By default, it will perform the + * following: + * + *
    + *
  • setup a new admin user which folks should use as opposed to the built-in + * vcloud account
  • + *
      + *
    • associate a random password to account
    • + *
        + *
      • securely ( use sha 512 on client side and literally rewrite the shadow + * entry, rather than pass password to OS in a script )
      • + *
      + *
    • associate the users' ssh public key with the account for login
    • + * associate it with the os group wheel
  • create os group wheel
  • + *
  • add sudoers for nopassword access to root by group wheel
  • reset + * root password securely
  • lockdown sshd_config for no root login, nor + * passwords allowed
+ * + * @author Adrian Cole + */ +public class AdminAccess implements Statement, Function { + public static AdminAccess.Builder builder() { + return new Builder(); + } + + public static AdminAccess.Builder builder(Function cryptFunction) { + return new Builder(cryptFunction); + } + + public static AdminAccess standard() { + return new Builder().build(); + } + + @ImplementedBy(DefaultConfiguration.class) + public static interface Configuration { + Supplier defaultAdminUsername(); + + Supplier> defaultAdminSshKeys(); + + Supplier passwordGenerator(); + + Function cryptFunction(); + } + + public static class Builder { + private final Function cryptFunction; + + public Builder() { + this(Sha512Crypt.function()); + } + + public Builder(Function cryptFunction) { + this.cryptFunction = cryptFunction; + } + + private String adminUsername; + private String adminPublicKey; + private File adminPublicKeyFile; + private String adminPrivateKey; + private File adminPrivateKeyFile; + private String adminPassword; + private String loginPassword; + private boolean lockSsh = true; + private boolean grantSudoToAdminUser = true; + private boolean authorizeAdminPublicKey = true; + private boolean installAdminPrivateKey = false; + private boolean resetLoginPassword = true; + + public AdminAccess.Builder adminUsername(String adminUsername) { + this.adminUsername = adminUsername; + return this; + } + + public AdminAccess.Builder adminPassword(String adminPassword) { + this.adminPassword = adminPassword; + return this; + } + + public AdminAccess.Builder loginPassword(String loginPassword) { + this.loginPassword = loginPassword; + return this; + } + + public AdminAccess.Builder lockSsh(boolean lockSsh) { + this.lockSsh = lockSsh; + return this; + } + + public AdminAccess.Builder resetLoginPassword(boolean resetLoginPassword) { + this.resetLoginPassword = resetLoginPassword; + return this; + } + + public AdminAccess.Builder authorizeAdminPublicKey(boolean authorizeAdminPublicKey) { + this.authorizeAdminPublicKey = authorizeAdminPublicKey; + return this; + } + + public AdminAccess.Builder installAdminPrivateKey(boolean installAdminPrivateKey) { + this.installAdminPrivateKey = installAdminPrivateKey; + return this; + } + + public AdminAccess.Builder grantSudoToAdminUser(boolean grantSudoToAdminUser) { + this.grantSudoToAdminUser = grantSudoToAdminUser; + return this; + } + + public AdminAccess.Builder adminPublicKey(File adminPublicKey) { + this.adminPublicKeyFile = adminPublicKey; + this.adminPublicKey = null; + return this; + } + + public AdminAccess.Builder adminPublicKey(String adminPublicKey) { + this.adminPublicKey = adminPublicKey; + this.adminPublicKeyFile = null; + return this; + } + + public AdminAccess.Builder adminPrivateKey(File adminPrivateKey) { + this.adminPrivateKeyFile = adminPrivateKey; + this.adminPrivateKey = null; + return this; + } + + public AdminAccess.Builder adminPrivateKey(String adminPrivateKey) { + this.adminPrivateKey = adminPrivateKey; + this.adminPrivateKeyFile = null; + return this; + } + + public AdminAccess build() { + try { + String adminPublicKey = this.adminPublicKey; + if (adminPublicKey == null && adminPublicKeyFile != null) + adminPublicKey = Files.toString(adminPublicKeyFile, UTF_8); + String adminPrivateKey = this.adminPrivateKey; + if (adminPrivateKey == null && adminPrivateKeyFile != null) + adminPrivateKey = Files.toString(adminPrivateKeyFile, UTF_8); + return new AdminAccess(adminUsername, adminPublicKey, adminPrivateKey, adminPassword, loginPassword, + lockSsh, grantSudoToAdminUser, authorizeAdminPublicKey, installAdminPrivateKey, resetLoginPassword, + cryptFunction); + } catch (IOException e) { + Throwables.propagate(e); + return null; + } + } + } + + private final String adminUsername; + private final String adminPublicKey; + private final String adminPrivateKey; + private final String adminPassword; + private final String loginPassword; + private final boolean lockSsh; + private final boolean grantSudoToAdminUser; + private final boolean authorizeAdminPublicKey; + private final boolean installAdminPrivateKey; + private final boolean resetLoginPassword; + private final Function cryptFunction; + private final Credentials adminCredentials; + + protected AdminAccess(@Nullable String adminUsername, @Nullable String adminPublicKey, + @Nullable String adminPrivateKey, @Nullable String adminPassword, @Nullable String loginPassword, + boolean lockSsh, boolean grantSudoToAdminUser, boolean authorizeAdminPublicKey, + boolean installAdminPrivateKey, boolean resetLoginPassword, Function cryptFunction) { + this.adminUsername = adminUsername; + this.adminPublicKey = adminPublicKey; + this.adminPrivateKey = adminPrivateKey; + this.adminPassword = adminPassword; + this.loginPassword = loginPassword; + this.lockSsh = lockSsh; + this.grantSudoToAdminUser = grantSudoToAdminUser; + this.authorizeAdminPublicKey = authorizeAdminPublicKey; + this.installAdminPrivateKey = installAdminPrivateKey; + this.resetLoginPassword = resetLoginPassword; + this.cryptFunction = cryptFunction; + if (adminUsername != null && authorizeAdminPublicKey && adminPrivateKey != null) + this.adminCredentials = new Credentials(adminUsername, adminPrivateKey); + else + this.adminCredentials = null; + } + + /** + * + * @return new credentials or null if unchanged or unavailable + */ + @Nullable + public Credentials getAdminCredentials() { + return adminCredentials; + } + + @Override + public Iterable functionDependencies(OsFamily family) { + return ImmutableList.of(); + } + + @Override + public AdminAccess apply(Configuration configuration) { + Builder builder = AdminAccess.builder(configuration.cryptFunction()); + builder.adminUsername(this.adminUsername != null ? this.adminUsername : configuration.defaultAdminUsername() + .get()); + builder.adminPassword(this.adminPassword != null ? this.adminPassword : configuration.passwordGenerator().get()); + Map adminSshKeys = (adminPublicKey != null && adminPrivateKey != null) ? ImmutableMap.of( + "public", adminPublicKey, "private", adminPrivateKey) : configuration.defaultAdminSshKeys().get(); + builder.adminPublicKey(adminSshKeys.get("public")); + builder.adminPrivateKey(adminSshKeys.get("private")); + builder.loginPassword(this.loginPassword != null ? this.loginPassword : configuration.passwordGenerator().get()); + builder.grantSudoToAdminUser(this.grantSudoToAdminUser); + builder.authorizeAdminPublicKey(this.authorizeAdminPublicKey); + builder.installAdminPrivateKey(this.installAdminPrivateKey); + builder.lockSsh(this.lockSsh); + builder.resetLoginPassword(this.resetLoginPassword); + return builder.build(); + } + + @Override + public String render(OsFamily family) { + checkNotNull(family, "family"); + if (family == OsFamily.WINDOWS) + throw new UnsupportedOperationException("windows not yet implemented"); + checkNotNull(adminUsername, "adminUsername"); + checkNotNull(adminPassword, "adminPassword"); + checkNotNull(adminPublicKey, "adminPublicKey"); + checkNotNull(adminPrivateKey, "adminPrivateKey"); + checkNotNull(loginPassword, "loginPassword"); + + ImmutableList.Builder statements = ImmutableList. builder(); + UserAdd.Builder userBuilder = UserAdd.builder(); + userBuilder.login(adminUsername); + if (authorizeAdminPublicKey) + userBuilder.authorizeRSAPublicKey(adminPublicKey); + userBuilder.password(adminPassword); + if (installAdminPrivateKey) + userBuilder.installRSAPrivateKey(adminPrivateKey); + if (grantSudoToAdminUser) { + statements.add(SudoStatements.createWheel()); + userBuilder.group("wheel"); + } + statements.add(userBuilder.build().cryptFunction(cryptFunction)); + if (lockSsh) + statements.add(SshStatements.lockSshd()); + if (resetLoginPassword) { + statements.add(ShadowStatements.resetLoginUserPasswordTo(loginPassword).cryptFunction(cryptFunction)); + } + return new StatementList(statements.build()).render(family); + } +} \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/DefaultConfiguration.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/DefaultConfiguration.java new file mode 100644 index 0000000000..c943df3b3d --- /dev/null +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/DefaultConfiguration.java @@ -0,0 +1,84 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.statements.login; + +import static com.google.common.base.Charsets.UTF_8; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import javax.inject.Singleton; + +import org.jclouds.crypto.Sha512Crypt; +import org.jclouds.crypto.SshKeys; +import org.jclouds.scriptbuilder.statements.login.AdminAccess.Configuration; +import org.jclouds.util.PasswordGenerator; + +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; + +/** + * + * @author Adrian Cole + * + */ +@Singleton +public class DefaultConfiguration implements Configuration { + + private final Supplier defaultAdminUsername = Suppliers.ofInstance(System.getProperty("user.name")); + private final Supplier> defaultAdminSshKeys = new Supplier>() { + + @Override + public Map get() { + try { + return ImmutableMap.of("public", + Files.toString(new File(System.getProperty("user.home") + "/.ssh/id_rsa.pub"), UTF_8), "private", + Files.toString(new File(System.getProperty("user.home") + "/.ssh/id_rsa"), UTF_8)); + } catch (IOException e) { + return SshKeys.generate(); + } + } + }; + private final Supplier passwordGenerator = PasswordGenerator.INSTANCE; + private final Function cryptFunction = Sha512Crypt.function(); + + @Override + public Supplier defaultAdminUsername() { + return defaultAdminUsername; + } + + @Override + public Supplier> defaultAdminSshKeys() { + return defaultAdminSshKeys; + } + + @Override + public Supplier passwordGenerator() { + return passwordGenerator; + } + + @Override + public Function cryptFunction() { + return cryptFunction; + } +} \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ReplaceShadowPasswordEntry.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ReplaceShadowPasswordEntry.java index a786837ee6..2b7514ba8b 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ReplaceShadowPasswordEntry.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ReplaceShadowPasswordEntry.java @@ -23,13 +23,17 @@ import static com.google.common.base.Throwables.propagate; import static java.lang.String.format; import static org.jclouds.scriptbuilder.domain.Statements.exec; -import org.jclouds.encryption.internal.JCECrypto; +import javax.inject.Named; + +import org.jclouds.crypto.Sha512Crypt; import org.jclouds.scriptbuilder.domain.OsFamily; import org.jclouds.scriptbuilder.domain.Statement; import org.jclouds.scriptbuilder.domain.StatementList; -import org.jclouds.scriptbuilder.util.Sha512Crypt; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; /** * Replaces the password entry for a user in the shadow file, using SHA-512 @@ -42,6 +46,16 @@ public class ReplaceShadowPasswordEntry implements Statement { private final String login; private final String password; + private Function cryptFunction = Sha512Crypt.function(); + + @Inject(optional = true) + @Named("CRYPT") + @VisibleForTesting + ReplaceShadowPasswordEntry cryptFunction(Function cryptFunction) { + this.cryptFunction = cryptFunction; + return this; + } + public ReplaceShadowPasswordEntry(String login, String password) { this.login = checkNotNull(login, "login"); this.password = checkNotNull(password, "password"); @@ -58,7 +72,7 @@ public class ReplaceShadowPasswordEntry implements Statement { if (family == OsFamily.WINDOWS) throw new UnsupportedOperationException("windows not yet implemented"); try { - String shadowPasswordEntry = Sha512Crypt.makeShadowLine(password, null, new JCECrypto()); + String shadowPasswordEntry = cryptFunction.apply(password); String shadowFile = "/etc/shadow"; // note we are using awk variables so that the user can be defined as a // shell variable (ex. $USER) As the block is in single quotes, diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ShadowStatements.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ShadowStatements.java index c19f832330..55f032a19e 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ShadowStatements.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/ShadowStatements.java @@ -18,7 +18,6 @@ */ package org.jclouds.scriptbuilder.statements.login; -import org.jclouds.scriptbuilder.domain.Statement; /** * Statements used to manipulate the shadow file @@ -31,7 +30,7 @@ public class ShadowStatements { * note must be run as root, and will either reset the root password, or * whoever sudoed to root. */ - public static Statement resetLoginUserPasswordTo(String password) { + public static ReplaceShadowPasswordEntryOfLoginUser resetLoginUserPasswordTo(String password) { return new ReplaceShadowPasswordEntryOfLoginUser(password); } } \ No newline at end of file diff --git a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/UserAdd.java b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/UserAdd.java index 1648ae13db..fb7a89b028 100644 --- a/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/UserAdd.java +++ b/scriptbuilder/src/main/java/org/jclouds/scriptbuilder/statements/login/UserAdd.java @@ -23,21 +23,24 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.util.List; import javax.annotation.Nullable; +import javax.inject.Named; -import org.jclouds.encryption.internal.JCECrypto; +import org.jclouds.crypto.Sha512Crypt; import org.jclouds.scriptbuilder.domain.OsFamily; import org.jclouds.scriptbuilder.domain.Statement; import org.jclouds.scriptbuilder.domain.StatementList; import org.jclouds.scriptbuilder.domain.Statements; import org.jclouds.scriptbuilder.statements.ssh.AuthorizeRSAPublicKeys; import org.jclouds.scriptbuilder.statements.ssh.InstallRSAPrivateKey; -import org.jclouds.scriptbuilder.util.Sha512Crypt; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.inject.Inject; /** * @@ -108,13 +111,13 @@ public class UserAdd implements Statement { } public UserAdd(String login, List groups, @Nullable String password, @Nullable String installRSAPrivateKey, - List authorizeRSAPublicKeys, String defaultHome, String shell) { + List authorizeRSAPublicKeys, String defaultHome, String shell) { this.login = checkNotNull(login, "login"); this.password = password; this.groups = ImmutableList.copyOf(checkNotNull(groups, "groups")); this.installRSAPrivateKey = installRSAPrivateKey; this.authorizeRSAPublicKeys = ImmutableList - .copyOf(checkNotNull(authorizeRSAPublicKeys, "authorizeRSAPublicKeys")); + .copyOf(checkNotNull(authorizeRSAPublicKeys, "authorizeRSAPublicKeys")); this.defaultHome = checkNotNull(defaultHome, "defaultHome"); this.shell = checkNotNull(shell, "shell"); } @@ -127,6 +130,16 @@ public class UserAdd implements Statement { private final List authorizeRSAPublicKeys; private final String shell; + private Function cryptFunction = Sha512Crypt.function(); + + @Inject(optional = true) + @Named("CRYPT") + @VisibleForTesting + UserAdd cryptFunction(Function cryptFunction) { + this.cryptFunction = cryptFunction; + return this; + } + @Override public Iterable functionDependencies(OsFamily family) { return ImmutableList.of(); @@ -157,7 +170,7 @@ public class UserAdd implements Statement { userAddOptions.put("-d", homeDir); if (password != null) { try { - userAddOptions.put("-p", "'" + Sha512Crypt.makeShadowLine(password, null, new JCECrypto()) + "'"); + userAddOptions.put("-p", "'" + cryptFunction.apply(password) + "'"); } catch (Exception e) { Throwables.propagate(e); } diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccessTest.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccessTest.java new file mode 100644 index 0000000000..ff055b7d22 --- /dev/null +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/CredentialsFromAdminAccessTest.java @@ -0,0 +1,84 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.functions; + +import static org.easymock.EasyMock.expect; +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; +import static org.easymock.classextension.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import org.jclouds.domain.Credentials; +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.Statements; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.testng.annotations.Test; + +/** + * @author Adrian Cole + */ +@Test(groups = "unit") +public class CredentialsFromAdminAccessTest { + + public void testWhenNotAdminAccess() { + + Statement statement = Statements.exec("echo hello"); + assertEquals(CredentialsFromAdminAccess.INSTANCE.apply(statement), null); + + Statement statementList = Statements.newStatementList(statement); + assertEquals(CredentialsFromAdminAccess.INSTANCE.apply(statementList), null); + + } + + public void testWhenAdminAccess() { + AdminAccess.Configuration configuration = createMock(AdminAccess.Configuration.class); + AdminAccess statement = createMock(AdminAccess.class); + Credentials creds = createMock(Credentials.class); + + expect(statement.getAdminCredentials()).andReturn(creds); + + replay(configuration); + replay(statement); + replay(creds); + + assertEquals(CredentialsFromAdminAccess.INSTANCE.apply(statement), creds); + + verify(configuration); + verify(statement); + verify(creds); + } + + public void testWhenAdminAccessInsideList() { + AdminAccess.Configuration configuration = createMock(AdminAccess.Configuration.class); + AdminAccess statement = createMock(AdminAccess.class); + Credentials creds = createMock(Credentials.class); + + expect(statement.getAdminCredentials()).andReturn(creds); + + replay(configuration); + replay(statement); + replay(creds); + + assertEquals(CredentialsFromAdminAccess.INSTANCE.apply(Statements.newStatementList(statement)), creds); + + verify(configuration); + verify(statement); + verify(creds); + } +} diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/InitAdminAccessTest.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/InitAdminAccessTest.java new file mode 100644 index 0000000000..3af9ba94d2 --- /dev/null +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/functions/InitAdminAccessTest.java @@ -0,0 +1,87 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.functions; + +import static org.easymock.EasyMock.expect; +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; +import static org.easymock.classextension.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import org.jclouds.scriptbuilder.domain.Statement; +import org.jclouds.scriptbuilder.domain.Statements; +import org.jclouds.scriptbuilder.statements.login.AdminAccess; +import org.testng.annotations.Test; + +/** + * @author Adrian Cole + */ +@Test(groups = "unit") +public class InitAdminAccessTest { + + public void testWhenNotAdminAccess() { + + InitAdminAccess initAdminAccess = new InitAdminAccess(createMock(AdminAccess.Configuration.class)); + Statement statement = Statements.exec("echo hello"); + assertEquals(initAdminAccess.apply(statement), statement); + + Statement statementList = Statements.newStatementList(statement); + assertEquals(initAdminAccess.apply(statementList), statementList); + + } + + public void testWhenAdminAccess() { + AdminAccess.Configuration configuration = createMock(AdminAccess.Configuration.class); + AdminAccess statement = createMock(AdminAccess.class); + AdminAccess newStatement = createMock(AdminAccess.class); + + expect(statement.apply(configuration)).andReturn(newStatement); + + replay(configuration); + replay(statement); + replay(newStatement); + InitAdminAccess initAdminAccess = new InitAdminAccess(configuration); + + assertEquals(initAdminAccess.apply(statement), newStatement); + + verify(configuration); + verify(statement); + verify(newStatement); + } + + public void testWhenAdminAccessInsideList() { + AdminAccess.Configuration configuration = createMock(AdminAccess.Configuration.class); + AdminAccess statement = createMock(AdminAccess.class); + AdminAccess newStatement = createMock(AdminAccess.class); + + expect(statement.apply(configuration)).andReturn(newStatement); + + replay(configuration); + replay(statement); + replay(newStatement); + InitAdminAccess initAdminAccess = new InitAdminAccess(configuration); + + assertEquals(initAdminAccess.apply(Statements.newStatementList(statement)), + Statements.newStatementList(newStatement)); + + verify(configuration); + verify(statement); + verify(newStatement); + } +} diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/AdminAccessTest.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/AdminAccessTest.java new file mode 100644 index 0000000000..078ac63065 --- /dev/null +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/AdminAccessTest.java @@ -0,0 +1,79 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.statements.login; + +import static org.testng.Assert.assertEquals; + +import java.io.IOException; + +import org.jclouds.scriptbuilder.domain.OsFamily; +import org.testng.annotations.Test; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; + +/** + * @author Adrian Cole + */ +@Test(groups = "unit", singleThreaded = true) +public class AdminAccessTest { + + public void testStandardUNIX() throws IOException { + TestConfiguration.INSTANCE.reset(); + try { + assertEquals(AdminAccess.standard().apply(TestConfiguration.INSTANCE).render(OsFamily.UNIX), + CharStreams.toString(Resources.newReaderSupplier(Resources.getResource("test_adminaccess_standard.sh"), + Charsets.UTF_8))); + } finally { + TestConfiguration.INSTANCE.reset(); + } + } + + public void testWithParamsUNIX() throws IOException { + TestConfiguration.INSTANCE.reset(); + try { + assertEquals( + AdminAccess.builder().adminPassword("bar").adminPrivateKey("fooPrivateKey") + .adminPublicKey("fooPublicKey").adminUsername("foo").build().apply(TestConfiguration.INSTANCE) + .render(OsFamily.UNIX), CharStreams.toString(Resources.newReaderSupplier( + Resources.getResource("test_adminaccess_params.sh"), Charsets.UTF_8))); + } finally { + TestConfiguration.INSTANCE.reset(); + } + } + + public void testOnlyInstallUserUNIX() throws IOException { + TestConfiguration.INSTANCE.reset(); + try { + assertEquals( + AdminAccess.builder().grantSudoToAdminUser(false).authorizeAdminPublicKey(true) + .installAdminPrivateKey(true).lockSsh(false).resetLoginPassword(false).build() + .apply(TestConfiguration.INSTANCE).render(OsFamily.UNIX), CharStreams.toString(Resources + .newReaderSupplier(Resources.getResource("test_adminaccess_plainuser.sh"), Charsets.UTF_8))); + } finally { + TestConfiguration.INSTANCE.reset(); + } + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testCreateWheelWindowsNotSupported() { + AdminAccess.standard().apply(TestConfiguration.INSTANCE).render(OsFamily.WINDOWS); + } +} diff --git a/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/TestConfiguration.java b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/TestConfiguration.java new file mode 100644 index 0000000000..d219ee157b --- /dev/null +++ b/scriptbuilder/src/test/java/org/jclouds/scriptbuilder/statements/login/TestConfiguration.java @@ -0,0 +1,84 @@ +/** + * + * Copyright (C) 2011 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed 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.jclouds.scriptbuilder.statements.login; + +import java.util.Map; + +import org.jclouds.scriptbuilder.statements.login.AdminAccess.Configuration; + +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; + +/** + * + * @author Adrian Cole + * + */ +public enum TestConfiguration implements Configuration { + INSTANCE; + int pwCount = 0; + + public TestConfiguration reset() { + pwCount = 0; + return this; + } + + private final Supplier defaultAdminUsername = Suppliers.ofInstance("defaultAdminUsername"); + private final Supplier> defaultAdminSshKeys = Suppliers + .> ofInstance(ImmutableMap.of("public", "publicKey", "private", "privateKey")); + private final Supplier passwordGenerator = new Supplier() { + + @Override + public String get() { + return pwCount++ + ""; + } + + }; + + private final Function cryptFunction = new Function() { + + @Override + public String apply(String input) { + return String.format("crypt(%s)", input); + } + + }; + + @Override + public Supplier defaultAdminUsername() { + return defaultAdminUsername; + } + + @Override + public Supplier> defaultAdminSshKeys() { + return defaultAdminSshKeys; + } + + @Override + public Supplier passwordGenerator() { + return passwordGenerator; + } + + @Override + public Function cryptFunction() { + return cryptFunction; + } +} \ No newline at end of file diff --git a/scriptbuilder/src/test/resources/test_adminaccess_flipped.sh b/scriptbuilder/src/test/resources/test_adminaccess_flipped.sh new file mode 100644 index 0000000000..0c10ee9582 --- /dev/null +++ b/scriptbuilder/src/test/resources/test_adminaccess_flipped.sh @@ -0,0 +1,21 @@ +rm /etc/sudoers +cat >> /etc/sudoers <<'END_OF_FILE' +root ALL = (ALL) ALL +%wheel ALL = (ALL) NOPASSWD:ALL +END_OF_FILE +chmod 0440 /etc/sudoers +mkdir -p /home/users/defaultAdminUsername +groupadd -f wheel +useradd -s /bin/bash -g wheel -d /home/users/defaultAdminUsername -p 'crypt(0)' defaultAdminUsername +mkdir -p /home/users/defaultAdminUsername/.ssh +cat >> /home/users/defaultAdminUsername/.ssh/authorized_keys <<'END_OF_FILE' +publicKey +END_OF_FILE +chmod 600 /home/users/defaultAdminUsername/.ssh/authorized_keys +chown -R defaultAdminUsername /home/users/defaultAdminUsername +exec 3<> /etc/ssh/sshd_config && awk -v TEXT="PasswordAuthentication no +PermitRootLogin no +" 'BEGIN {print TEXT}{print}' /etc/ssh/sshd_config >&3 +/etc/init.d/sshd reload||/etc/init.d/ssh reload +awk -v user=^${SUDO_USER:=${USER}}: -v password='crypt(1)' 'BEGIN { FS=OFS=":" } $0 ~ user { $2 = password } 1' /etc/shadow >/etc/shadow.${SUDO_USER:=${USER}} +test -f /etc/shadow.${SUDO_USER:=${USER}} && mv /etc/shadow.${SUDO_USER:=${USER}} /etc/shadow diff --git a/scriptbuilder/src/test/resources/test_adminaccess_params.sh b/scriptbuilder/src/test/resources/test_adminaccess_params.sh new file mode 100644 index 0000000000..5c66dc833f --- /dev/null +++ b/scriptbuilder/src/test/resources/test_adminaccess_params.sh @@ -0,0 +1,21 @@ +rm /etc/sudoers +cat >> /etc/sudoers <<'END_OF_FILE' +root ALL = (ALL) ALL +%wheel ALL = (ALL) NOPASSWD:ALL +END_OF_FILE +chmod 0440 /etc/sudoers +mkdir -p /home/users/foo +groupadd -f wheel +useradd -s /bin/bash -g wheel -d /home/users/foo -p 'crypt(bar)' foo +mkdir -p /home/users/foo/.ssh +cat >> /home/users/foo/.ssh/authorized_keys <<'END_OF_FILE' +fooPublicKey +END_OF_FILE +chmod 600 /home/users/foo/.ssh/authorized_keys +chown -R foo /home/users/foo +exec 3<> /etc/ssh/sshd_config && awk -v TEXT="PasswordAuthentication no +PermitRootLogin no +" 'BEGIN {print TEXT}{print}' /etc/ssh/sshd_config >&3 +/etc/init.d/sshd reload||/etc/init.d/ssh reload +awk -v user=^${SUDO_USER:=${USER}}: -v password='crypt(0)' 'BEGIN { FS=OFS=":" } $0 ~ user { $2 = password } 1' /etc/shadow >/etc/shadow.${SUDO_USER:=${USER}} +test -f /etc/shadow.${SUDO_USER:=${USER}} && mv /etc/shadow.${SUDO_USER:=${USER}} /etc/shadow diff --git a/scriptbuilder/src/test/resources/test_adminaccess_plainuser.sh b/scriptbuilder/src/test/resources/test_adminaccess_plainuser.sh new file mode 100644 index 0000000000..9909b98bf9 --- /dev/null +++ b/scriptbuilder/src/test/resources/test_adminaccess_plainuser.sh @@ -0,0 +1,14 @@ +mkdir -p /home/users/defaultAdminUsername +useradd -s /bin/bash -d /home/users/defaultAdminUsername -p 'crypt(0)' defaultAdminUsername +mkdir -p /home/users/defaultAdminUsername/.ssh +cat >> /home/users/defaultAdminUsername/.ssh/authorized_keys <<'END_OF_FILE' +publicKey +END_OF_FILE +chmod 600 /home/users/defaultAdminUsername/.ssh/authorized_keys +mkdir -p /home/users/defaultAdminUsername/.ssh +rm /home/users/defaultAdminUsername/.ssh/id_rsa +cat >> /home/users/defaultAdminUsername/.ssh/id_rsa <<'END_OF_FILE' +privateKey +END_OF_FILE +chmod 600 /home/users/defaultAdminUsername/.ssh/id_rsa +chown -R defaultAdminUsername /home/users/defaultAdminUsername diff --git a/scriptbuilder/src/test/resources/test_adminaccess_standard.sh b/scriptbuilder/src/test/resources/test_adminaccess_standard.sh new file mode 100644 index 0000000000..0c10ee9582 --- /dev/null +++ b/scriptbuilder/src/test/resources/test_adminaccess_standard.sh @@ -0,0 +1,21 @@ +rm /etc/sudoers +cat >> /etc/sudoers <<'END_OF_FILE' +root ALL = (ALL) ALL +%wheel ALL = (ALL) NOPASSWD:ALL +END_OF_FILE +chmod 0440 /etc/sudoers +mkdir -p /home/users/defaultAdminUsername +groupadd -f wheel +useradd -s /bin/bash -g wheel -d /home/users/defaultAdminUsername -p 'crypt(0)' defaultAdminUsername +mkdir -p /home/users/defaultAdminUsername/.ssh +cat >> /home/users/defaultAdminUsername/.ssh/authorized_keys <<'END_OF_FILE' +publicKey +END_OF_FILE +chmod 600 /home/users/defaultAdminUsername/.ssh/authorized_keys +chown -R defaultAdminUsername /home/users/defaultAdminUsername +exec 3<> /etc/ssh/sshd_config && awk -v TEXT="PasswordAuthentication no +PermitRootLogin no +" 'BEGIN {print TEXT}{print}' /etc/ssh/sshd_config >&3 +/etc/init.d/sshd reload||/etc/init.d/ssh reload +awk -v user=^${SUDO_USER:=${USER}}: -v password='crypt(1)' 'BEGIN { FS=OFS=":" } $0 ~ user { $2 = password } 1' /etc/shadow >/etc/shadow.${SUDO_USER:=${USER}} +test -f /etc/shadow.${SUDO_USER:=${USER}} && mv /etc/shadow.${SUDO_USER:=${USER}} /etc/shadow