GCE ResetWindowsPassword - Function which tells to GCE to reset Windows password
- InstanceApiLiveTest for windows - Unit test for decrypting windows password Project: http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/commit/276ee07e Tree: http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/tree/276ee07e Diff: http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/diff/276ee07e Branch: refs/heads/1.9.x Commit: 276ee07e57a4851e8e93e1f64c85f1caa3ff848a Parents: 8d64843 Author: Valentin Aitken <[email protected]> Authored: Tue Mar 29 03:42:20 2016 +0300 Committer: Andrea Turli <[email protected]> Committed: Mon Jun 6 12:05:19 2016 +0200 ---------------------------------------------------------------------- google-compute-engine/pom.xml | 6 + .../GoogleComputeEngineApiMetadata.java | 2 +- .../GoogleComputeEngineServiceAdapter.java | 37 ++- ...GoogleComputeEngineServiceContextModule.java | 6 +- .../compute/functions/ResetWindowsPassword.java | 226 +++++++++++++++++++ .../GoogleComputeEngineTemplateOptions.java | 24 +- .../features/InstanceApi.java | 11 + .../functions/ResetWindowsPasswordTest.java | 105 +++++++++ .../features/InstanceApiMockTest.java | 12 + .../features/InstanceApiWindowsLiveTest.java | 132 +++++++++++ .../BaseGoogleComputeEngineApiLiveTest.java | 3 +- .../parse/ParseInstanceSerialOutputTest.java | 8 + .../instance_serial_port_4_windows.json | 5 + 13 files changed, 562 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/pom.xml ---------------------------------------------------------------------- diff --git a/google-compute-engine/pom.xml b/google-compute-engine/pom.xml index a89e805..0ad083b 100644 --- a/google-compute-engine/pom.xml +++ b/google-compute-engine/pom.xml @@ -115,6 +115,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.apache.jclouds.driver</groupId> + <artifactId>jclouds-bouncycastle</artifactId> + <version>${jclouds.version}</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>com.squareup.okhttp</groupId> <artifactId>mockwebserver</artifactId> <scope>test</scope> http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/GoogleComputeEngineApiMetadata.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/GoogleComputeEngineApiMetadata.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/GoogleComputeEngineApiMetadata.java index c2e632a..ce50e06 100644 --- a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/GoogleComputeEngineApiMetadata.java +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/GoogleComputeEngineApiMetadata.java @@ -61,7 +61,7 @@ public class GoogleComputeEngineApiMetadata extends BaseHttpApiMetadata<GoogleCo properties.put(JWS_ALG, "RS256"); properties.put(PROPERTY_SESSION_INTERVAL, 3600); properties.put(OPERATION_COMPLETE_INTERVAL, 500); - properties.put(OPERATION_COMPLETE_TIMEOUT, 600000); + properties.put(OPERATION_COMPLETE_TIMEOUT, 1000000); properties.put(TEMPLATE, "osFamily=DEBIAN,osVersionMatches=7\\..*,locationId=us-central1-a"); properties.put(PROJECT_NAME, ""); // Defaulting to empty helps avoid temptation for optional inject! properties.put(IMAGE_PROJECTS, "centos-cloud,debian-cloud,rhel-cloud,suse-cloud,opensuse-cloud,gce-nvme,coreos-cloud,ubuntu-os-cloud,windows-cloud"); http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/GoogleComputeEngineServiceAdapter.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/GoogleComputeEngineServiceAdapter.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/GoogleComputeEngineServiceAdapter.java index ee50d92..8dfab09 100644 --- a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/GoogleComputeEngineServiceAdapter.java +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/GoogleComputeEngineServiceAdapter.java @@ -24,24 +24,19 @@ import static java.lang.String.format; import static org.jclouds.googlecloud.internal.ListPages.concat; import static org.jclouds.googlecomputeengine.config.GoogleComputeEngineProperties.IMAGE_PROJECTS; -import javax.inject.Inject; -import javax.inject.Named; import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import com.google.common.base.Predicate; -import com.google.common.base.Splitter; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.util.concurrent.Atomics; -import com.google.common.util.concurrent.UncheckedTimeoutException; +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.collect.ImmutableMap; import org.jclouds.compute.ComputeServiceAdapter; import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.NodeMetadata; +import org.jclouds.compute.domain.OsFamily; import org.jclouds.compute.domain.Template; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.domain.Location; @@ -66,6 +61,16 @@ import org.jclouds.googlecomputeengine.domain.Zone; import org.jclouds.googlecomputeengine.features.InstanceApi; import org.jclouds.location.suppliers.all.JustProvider; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Atomics; +import com.google.common.util.concurrent.UncheckedTimeoutException; + /** * This implementation maps the following: * <ul> @@ -87,6 +92,7 @@ public final class GoogleComputeEngineServiceAdapter private final Map<URI, URI> diskToSourceImage; private final Predicate<AtomicReference<Operation>> operationDone; private final Predicate<AtomicReference<Instance>> instanceVisible; + private final Function<Map<String, ?>, String> windowsPasswordGenerator; private final FirewallTagNamingConvention.Factory firewallTagNamingConvention; private final List<String> imageProjects; @@ -94,6 +100,7 @@ public final class GoogleComputeEngineServiceAdapter Predicate<AtomicReference<Operation>> operationDone, Predicate<AtomicReference<Instance>> instanceVisible, Map<URI, URI> diskToSourceImage, + Function<Map<String, ?>, String> windowsPasswordGenerator, Resources resources, FirewallTagNamingConvention.Factory firewallTagNamingConvention, @Named(IMAGE_PROJECTS) String imageProjects) { @@ -102,6 +109,7 @@ public final class GoogleComputeEngineServiceAdapter this.operationDone = operationDone; this.instanceVisible = instanceVisible; this.diskToSourceImage = diskToSourceImage; + this.windowsPasswordGenerator = windowsPasswordGenerator; this.resources = resources; this.firewallTagNamingConvention = firewallTagNamingConvention; this.imageProjects = Splitter.on(',').omitEmptyStrings().splitToList(imageProjects); @@ -173,6 +181,15 @@ public final class GoogleComputeEngineServiceAdapter // Add lookup for InstanceToNodeMetadata diskToSourceImage.put(instance.get().disks().get(0).source(), template.getImage().getUri()); + if (options.autoCreateWindowsPassword() != null && options.autoCreateWindowsPassword() + || OsFamily.WINDOWS == template.getImage().getOperatingSystem().getFamily()) { + Map<String, ?> params = ImmutableMap.of("instance", instance, "zone", zone, "email", create.user(), "userName", credentials.getUser()); + String password = windowsPasswordGenerator.apply(params); + credentials = LoginCredentials.builder(credentials) + .password(password) + .build(); + } + return new NodeAndInitialCredentials<Instance>(instance.get(), instance.get().selfLink().toString(), credentials); } http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/config/GoogleComputeEngineServiceContextModule.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/config/GoogleComputeEngineServiceContextModule.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/config/GoogleComputeEngineServiceContextModule.java index 1e22b29..0da5ce4 100644 --- a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/config/GoogleComputeEngineServiceContextModule.java +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/config/GoogleComputeEngineServiceContextModule.java @@ -58,10 +58,11 @@ import org.jclouds.googlecomputeengine.compute.functions.InstanceToNodeMetadata; import org.jclouds.googlecomputeengine.compute.functions.MachineTypeToHardware; import org.jclouds.googlecomputeengine.compute.functions.OrphanedGroupsFromDeadNodes; import org.jclouds.googlecomputeengine.compute.functions.Resources; +import org.jclouds.googlecomputeengine.compute.functions.ResetWindowsPassword; import org.jclouds.googlecomputeengine.compute.options.GoogleComputeEngineTemplateOptions; -import org.jclouds.googlecomputeengine.compute.predicates.GroupIsEmpty; import org.jclouds.googlecomputeengine.compute.predicates.AtomicInstanceVisible; import org.jclouds.googlecomputeengine.compute.predicates.AtomicOperationDone; +import org.jclouds.googlecomputeengine.compute.predicates.GroupIsEmpty; import org.jclouds.googlecomputeengine.compute.strategy.CreateNodesWithGroupEncodedIntoNameThenAddToSet; import org.jclouds.googlecomputeengine.domain.Image; import org.jclouds.googlecomputeengine.domain.Instance; @@ -134,6 +135,9 @@ public final class GoogleComputeEngineServiceContextModule bind(new TypeLiteral<CacheLoader<NetworkAndAddressRange, Network>>() { }).to(FindNetworkOrCreate.class); + bind(new TypeLiteral<Function<Map<String, ?>, String>>() { + }).to(ResetWindowsPassword.class); + bind(FirewallTagNamingConvention.Factory.class).in(Scopes.SINGLETON); bindHttpApi(binder(), Resources.class); } http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPassword.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPassword.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPassword.java new file mode 100644 index 0000000..cd25fa1 --- /dev/null +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPassword.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.googlecomputeengine.compute.functions; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.Resource; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.gson.GsonBuilder; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.crypto.Crypto; +import org.jclouds.googlecomputeengine.GoogleComputeEngineApi; +import org.jclouds.googlecomputeengine.domain.Instance; +import org.jclouds.googlecomputeengine.domain.Instance.SerialPortOutput; +import org.jclouds.googlecomputeengine.domain.Metadata; +import org.jclouds.googlecomputeengine.domain.Operation; +import org.jclouds.googlecomputeengine.features.InstanceApi; +import org.jclouds.logging.Logger; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.Atomics; +import com.google.gson.Gson; +import org.jclouds.util.Predicates2; + +/** + * References: + * <ul> + * <li><a href="https://cloud.google.com/compute/docs/instances/automate-pw-generation">automate-pw-generation</a> + * <li><a href="https://github.com/GoogleCloudPlatform/compute-image-windows/blob/master/examples/windows_auth_java_sample.java">windows_auth_java_sample.java</a> + * </ul> + * + * In brief, the sequence is: + * <ol> + * <li>Generate a temporary key for encrypting and decrypting the password + * <li>Send the RSA public key to the instance, by settings its metadata + * <li>Retrieve the result from the {@link SerialPortOutput} + * <li>Decode and decrypt the result. + * </ol> + */ +public class ResetWindowsPassword implements Function<Map<String, ?>, String> { + + /** + * Indicates when the key should expire. Keys are one-time use, so the metadata doesn't need to stay around for long. + * 10 minutes chosen to allow for differences between time on the client + * and time on the server. + */ + private static final long EXPIRE_DURATION = 10 * 60 * 1000; + + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + private final GoogleComputeEngineApi api; + private final Crypto crypto; + private final Predicate<AtomicReference<Operation>> operationDone; + + @Inject + protected ResetWindowsPassword(GoogleComputeEngineApi api, Crypto crypto, Predicate<AtomicReference<Operation>> operationDone) { + this.api = api; + this.crypto = crypto; + this.operationDone = operationDone; + } + + @Override + public String apply(Map<String, ?> params) { + String zone = (String)params.get("zone"); + final AtomicReference<Instance> instance = (AtomicReference<Instance>)params.get("instance"); + String userName = (String)params.get("userName"); + String email = (String)params.get("email"); + + // Generate the public/private key pair for encryption and decryption. + // TODO do we need to explicitly set 2048 bits? Presumably "RSA" is implicit + KeyPair keys = crypto.rsaKeyPairGenerator().genKeyPair(); + + // Update instance's metadata with new "windows-keys" entry, and wait for operation to + // complete. + logger.debug("Generating windows key for instance %s, by updating metadata", instance.get().name()); + final InstanceApi instanceApi = api.instancesInZone(zone); + Metadata metadata = instance.get().metadata(); + + try { + // If disableHtmlEscaping is not there, == will be escaped from modulus value + metadata.put("windows-keys", new GsonBuilder().disableHtmlEscaping().create().toJson(extractKeyMetadata(keys, userName, email))); + } catch (NoSuchAlgorithmException e) { + Throwables.propagate(e); + } catch (InvalidKeySpecException e) { + Throwables.propagate(e); + } + + AtomicReference<Operation> operation = Atomics.newReference(instanceApi.setMetadata(instance.get().name(), metadata)); + operationDone.apply(operation); + + if (operation.get().httpErrorStatusCode() != null) { + logger.warn("Generating windows key for %s failed. Http Error Code: %d HttpError: %s", + operation.get().targetId(), operation.get().httpErrorStatusCode(), + operation.get().httpErrorMessage()); + } + + try { + final Map<String, String> passwordDict = new HashMap<String, String>(); + boolean passwordRetrieved = Predicates2.retry(new Predicate<Instance>() { + public boolean apply(Instance instance) { + String serialPortContents = instanceApi.getSerialPortOutput(instance.name(), 4).contents(); + if (!serialPortContents.startsWith("{\"ready\":true")) { + return false; + } + String[] contentEntries = serialPortContents.split("\n"); + passwordDict.clear(); + passwordDict.putAll(new Gson().fromJson(contentEntries[contentEntries.length - 1], Map.class)); + passwordDict.put("passwordDictContentEntries", contentEntries[contentEntries.length - 1]); + return passwordDict.get("encryptedPassword") != null; + } + }, 10 * 60, 30, TimeUnit.SECONDS).apply(instance.get()); // Notice that timeoutDuration should be less than EXPIRE_DURATION + if (passwordRetrieved) { + return decryptPassword(checkNotNull(passwordDict.get("encryptedPassword"), "encryptedPassword shouldn't be null"), keys); + } else { + throw new IllegalStateException("encryptedPassword shouldn't be null: " + passwordDict.get("passwordDictContentEntries")); + } + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + /** + * Decrypts the given password - the encrypted text is base64-encoded. + * As per the GCE docs, assumes it was encrypted with algorithm "RSA/NONE/OAEPPadding", and UTF-8. + */ + protected String decryptPassword(String message, KeyPair keys) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher; + try { + // Assumes user has configured appropriate crypto guice module. + cipher = crypto.cipher("RSA/NONE/OAEPPadding"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Problem finding cypher. Try adding bouncycastle dependency.", e); + } catch (NoSuchPaddingException e) { + throw new RuntimeException("Problem finding cypher. Try adding bouncycastle dependency.", e); + } + + // Add the private key for decryption. + cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate()); + + // Decrypt the text. + byte[] rawMessage = BaseEncoding.base64().decode(message); + byte[] decryptedText = cipher.doFinal(rawMessage); + + // The password was encoded using UTF8. Transform into string. + return new String(decryptedText, Charset.forName("UTF-8")); + } + + /** + * Generates the metadata value for this keypair. + * Extracts the public key's the RSA spec's modulus and exponent, encoded as Base-64, and + * an expires date. + * + * @param pair + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + protected Map<String, String> extractKeyMetadata(KeyPair pair, String userName, String email) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = crypto.rsaKeyFactory(); + RSAPublicKeySpec pubSpec = factory.getKeySpec(pair.getPublic(), RSAPublicKeySpec.class); + BigInteger modulus = pubSpec.getModulus(); + BigInteger exponent = pubSpec.getPublicExponent(); + + // Strip out the leading 0 byte in the modulus. + byte[] modulusArr = Arrays.copyOfRange(modulus.toByteArray(), 1, modulus.toByteArray().length); + String modulusString = BaseEncoding.base64().encode(modulusArr).replaceAll("\n", ""); + String exponentString = BaseEncoding.base64().encode(exponent.toByteArray()).replaceAll("\n", ""); + + // Create the expire date, formatted as rfc3339 + Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_DURATION); + SimpleDateFormat rfc3339Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + rfc3339Format.setTimeZone(TimeZone.getTimeZone("UTC")); + String expireString = rfc3339Format.format(expireDate); + + return ImmutableMap.<String, String>builder() + .put("modulus", modulusString) + .put("exponent", exponentString) + .put("expireOn", expireString) + .put("userName", userName) + .put("email", email) // email of the user should be here. Now it is the username. + .build(); + } +} http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/options/GoogleComputeEngineTemplateOptions.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/options/GoogleComputeEngineTemplateOptions.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/options/GoogleComputeEngineTemplateOptions.java index 7b4350e..6afb45f 100644 --- a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/options/GoogleComputeEngineTemplateOptions.java +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/compute/options/GoogleComputeEngineTemplateOptions.java @@ -29,6 +29,7 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions { private URI network = null; private boolean autoCreateKeyPair = true; + private Boolean autoCreateWindowsPassword = null; private String bootDiskType; private boolean preemptible = false; @@ -46,6 +47,7 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions { GoogleComputeEngineTemplateOptions eTo = GoogleComputeEngineTemplateOptions.class.cast(to); eTo.network(network()); eTo.autoCreateKeyPair(autoCreateKeyPair()); + eTo.autoCreateWindowsPassword(autoCreateWindowsPassword()); eTo.bootDiskType(bootDiskType()); eTo.preemptible(preemptible()); } @@ -101,12 +103,30 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions { } /** - * Sets whether an SSH key pair should be created automatically. + * Whether an SSH key pair should be created automatically. */ public boolean autoCreateKeyPair() { return autoCreateKeyPair; } - + + /** + * Whether a Windows password should be created automatically; {@link null} means to generate + * the password if and only if the image is for a Windows VM. + */ + public Boolean autoCreateWindowsPassword() { + return autoCreateWindowsPassword; + } + + /** + * Sets whether to auto-create a windows password. The default ({@code null}) is to always + * do so for Windows VMs, inferring this from the image. An explicit value of true or false + * overrides this. + */ + public GoogleComputeEngineTemplateOptions autoCreateWindowsPassword(Boolean autoCreateWindowsPassword) { + this.autoCreateWindowsPassword = autoCreateWindowsPassword; + return this; + } + /** * {@inheritDoc} */ http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/features/InstanceApi.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/features/InstanceApi.java b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/features/InstanceApi.java index 43d708a..99fc284 100644 --- a/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/features/InstanceApi.java +++ b/google-compute-engine/src/main/java/org/jclouds/googlecomputeengine/features/InstanceApi.java @@ -135,6 +135,17 @@ public interface InstanceApi { @GET @Path("/{instance}/serialPort") SerialPortOutput getSerialPortOutput(@PathParam("instance") String instance); + + /** + * Returns the specified instance's serial port output. + * + * @param instance the instance name. + * @return if successful, this method returns a SerialPortOutput containing the instance's serial output. + */ + @Named("Instances:getSerialPortOutput") + @GET + @Path("/{instance}/serialPort") + SerialPortOutput getSerialPortOutput(@PathParam("instance") String instance, @QueryParam("port") int port); /** * Hard-resets the instance. http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPasswordTest.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPasswordTest.java b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPasswordTest.java new file mode 100644 index 0000000..0b3495f --- /dev/null +++ b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/compute/functions/ResetWindowsPasswordTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.googlecomputeengine.compute.functions; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.spec.RSAPublicKeySpec; +import java.util.concurrent.atomic.AtomicReference; + +import javax.crypto.Cipher; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import org.jclouds.crypto.Crypto; +import org.jclouds.encryption.bouncycastle.BouncyCastleCrypto; +import org.jclouds.googlecomputeengine.GoogleComputeEngineApi; +import org.jclouds.googlecomputeengine.domain.Instance; +import org.jclouds.googlecomputeengine.domain.Instance.SerialPortOutput; +import org.jclouds.googlecomputeengine.domain.Metadata; +import org.jclouds.googlecomputeengine.domain.Operation; +import org.jclouds.googlecomputeengine.features.InstanceApi; +import org.jclouds.googlecomputeengine.parse.ParseInstanceTest; +import org.testng.annotations.Test; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +@Test(groups = "unit") +public class ResetWindowsPasswordTest { + public void testGeneratePassword() throws Exception { + Crypto bcCrypto = new BouncyCastleCrypto(); + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + + String password = "|opj213'33423'*"; + + KeyPair keyPair = bcCrypto.rsaKeyPairGenerator().genKeyPair(); + + KeyFactory factory = bcCrypto.rsaKeyFactory(); + RSAPublicKeySpec pubSpec = factory.getKeySpec(keyPair.getPublic(), RSAPublicKeySpec.class); + BigInteger exponent = pubSpec.getPublicExponent(); + String exponentString = BaseEncoding.base64().encode(exponent.toByteArray()).replaceAll("\n", ""); + + Cipher cipher = bcCrypto.cipher("RSA/NONE/OAEPPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic()); + String encryptedPass = BaseEncoding.base64().encode(cipher.doFinal(password.getBytes(Charset.forName("UTF-8")), 0, password.length())); + + Predicate<AtomicReference<Operation>> operationDone = Predicates.alwaysTrue(); + Instance instance = new ParseInstanceTest().expected(); + String zone = "us-central1-a"; + + GoogleComputeEngineApi api = createMock(GoogleComputeEngineApi.class); + InstanceApi instanceApi = createMock(InstanceApi.class); + Operation operation = createMock(Operation.class); + SerialPortOutput serialPortOutput = createMock(SerialPortOutput.class); + Crypto crypto = createMock(Crypto.class); + KeyPairGenerator keyPairGenerator = createMock(KeyPairGenerator.class); + + expect(api.instancesInZone(zone)).andReturn(instanceApi).atLeastOnce(); + expect(crypto.rsaKeyPairGenerator()).andReturn(keyPairGenerator); + expect(crypto.rsaKeyFactory()).andReturn(factory); + expect(keyPairGenerator.genKeyPair()).andReturn(keyPair); + // FIXME assert that metadata contained what we expected + expect(instanceApi.setMetadata(eq(instance.name()), isA(Metadata.class))).andReturn(operation).atLeastOnce(); + expect(operation.httpErrorStatusCode()).andReturn(null); + expect(instanceApi.getSerialPortOutput(instance.name(), 4)).andReturn(serialPortOutput).atLeastOnce(); + expect(serialPortOutput.contents()).andReturn("{\"ready\":true,\"version\":\"Microsoft Windows NT 6.2.9200.0\"}\n" + + "{\"encryptedPassword\":\"" + encryptedPass + "\",\"exponent\":\"" + exponentString + "\",\"passwordFound\":true,\"userName\":\"Administrator\"}"); + expect(crypto.cipher("RSA/NONE/OAEPPadding")).andReturn(bcCrypto.cipher("RSA/NONE/OAEPPadding")); + + replay(api, instanceApi, operation, serialPortOutput, crypto, keyPairGenerator); + + ResetWindowsPassword generator = new ResetWindowsPassword(api, crypto, operationDone); + String result = generator.apply(ImmutableMap.of("instance", new AtomicReference<Instance>(instance), "zone", zone, "email", "[email protected]", "userName", "test")); + + verify(api, instanceApi, operation, serialPortOutput); + + assertEquals(result, password); + } +} http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiMockTest.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiMockTest.java b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiMockTest.java index 1bc3821..e59c7f5 100644 --- a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiMockTest.java +++ b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiMockTest.java @@ -65,6 +65,18 @@ public class InstanceApiMockTest extends BaseGoogleComputeEngineApiMockTest { assertSent(server, "GET", "/projects/party/zones/us-central1-a/instances/test-1/serialPort"); } + public void getInstanceSerialPortOutputWindowsPassword() throws Exception { + server.enqueue(jsonResponse("/instance_serial_port_4_windows.json")); + + assertEquals(instanceApi().getSerialPortOutput("test-1", 4), + new ParseInstanceSerialOutputTest().expected( + url("/projects"), + "{\"ready\":true,\"version\":\"Microsoft Windows NT 6.1.7601 Service Pack 1\"}\n{\"encryptedPassword\":\"uiHDEhxyvj6lF5GalHh9TsMZb4bG6Y9qGmFb9S3XI29yvVsDCLdp4IbUg21MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5TxQo9hL0lBW7o3FRM\\/UIXCeRk39ObUl2AjDmQ0mcw1byJI5v9KVJnNMaHdRCy\\/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjSbvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXrfIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==\",\"exponent\":\"AQAB\",\"modulus\":\"0tiKdO2JmBHss26jnrSAwb583KG\\/ZIw5JwwMPXrCVsFAPwY1OV3RlT1Hp4Xvpibr7rvJbOC+f\\/Gd0cBrK5pccQfccB+OHKpbBof473zEfRbdtFwPn10RfAFj\\/xikW0r\\/XxgG\\/c8tz9bmALBStGqmwOVOLRHxjwgtGu4poeuwmFfG6TuwgCadxpllW74mviFd4LZVSuCSni5YJnBM2HSJ8NP6g1fqI17KDXt2XO\\/7kSItubmMk+HGEXdH4qiugHYewaIf1o4XSQROC8xlRl7t\\/RaD4U58hKYkVwg0Ir7WzYzAVpG2UR4Co\\/GDG9Hct7HOYekDqVQ+sSZbwzajnVunkw==\",\"passwordFound\":true,\"userName\":\"example-user\"}\n" + )); + + assertSent(server, "GET", "/projects/party/zones/us-central1-a/instances/test-1/serialPort?port=4"); + } + public void insert_noOptions() throws Exception { server.enqueue(jsonResponse("/zone_operation.json")); http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiWindowsLiveTest.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiWindowsLiveTest.java b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiWindowsLiveTest.java new file mode 100644 index 0000000..5062186 --- /dev/null +++ b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/features/InstanceApiWindowsLiveTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.googlecomputeengine.features; + +import static org.jclouds.googlecomputeengine.options.ListOptions.Builder.filter; +import static org.testng.Assert.assertFalse; + +import java.net.URI; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import org.jclouds.encryption.bouncycastle.config.BouncyCastleCryptoModule; +import org.jclouds.googlecomputeengine.GoogleComputeEngineApi; +import org.jclouds.googlecomputeengine.domain.Image; +import org.jclouds.googlecomputeengine.domain.Instance; +import org.jclouds.googlecomputeengine.domain.NewInstance; +import org.jclouds.googlecomputeengine.internal.BaseGoogleComputeEngineApiLiveTest; +import org.jclouds.googlecomputeengine.options.DiskCreationOptions; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; + +@Test(groups = "live", testName = "InstanceApiLiveTest") +public class InstanceApiWindowsLiveTest extends BaseGoogleComputeEngineApiLiveTest { + + private static final String INSTANCE_NETWORK_NAME = "instance-api-live-test-network"; + private static final String INSTANCE_NAME = "instance-api-test-instance-1"; + private static final String DISK_NAME = "instance-live-test-disk"; + private static final String IPV4_RANGE = "10.0.0.0/8"; + private static final int DEFAULT_DISK_SIZE_GB = 25; + + private Function<Map<String, ?>, String> reset_windows_password; + private NewInstance instance; + + @Override + protected GoogleComputeEngineApi create(Properties props, Iterable<Module> modules) { + GoogleComputeEngineApi api = super.create(props, modules); + reset_windows_password = injector.getInstance(Key.get(new TypeLiteral<Function<Map<String, ?>, String>>() {})); + + List<Image> list = api.images().listInProject("windows-cloud", filter("name eq windows-server-2012.*")).next(); + URI imageUri = FluentIterable.from(list) + .filter(new Predicate<Image>() { + @Override + public boolean apply(Image input) { + // filter out all deprecated images + return !(input.deprecated() != null && input.deprecated().state() != null); + } + }) + .first() + .get() + .selfLink(); + instance = NewInstance.create( + INSTANCE_NAME, + getDefaultMachineTypeUrl(), + getNetworkUrl(INSTANCE_NETWORK_NAME), + imageUri + ); + + return api; + } + + @Override + protected Iterable<Module> setupModules() { + return ImmutableSet.<Module>builder().addAll(super.setupModules()).add(new BouncyCastleCryptoModule()).build(); + } + + private InstanceApi api() { + return api.instancesInZone(DEFAULT_ZONE_NAME); + } + + private DiskApi diskApi() { + return api.disksInZone(DEFAULT_ZONE_NAME); + } + + @Test(groups = "live") + public void testInsertInstanceWindows() { + // need to insert the network first + assertOperationDoneSuccessfully(api.networks().createInIPv4Range + (INSTANCE_NETWORK_NAME, IPV4_RANGE)); + + assertOperationDoneSuccessfully(diskApi().create(DISK_NAME, + new DiskCreationOptions.Builder().sizeGb(DEFAULT_DISK_SIZE_GB).build())); + assertOperationDoneSuccessfully(api().create(instance)); + } + + @Test(groups = "live", dependsOnMethods = "testInsertInstanceWindows") + public void testGetSerialPortOutput4() throws NoSuchAlgorithmException, CertificateException { + Instance instance = api().get(INSTANCE_NAME); + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // Needed when initializing the cipher + String result = reset_windows_password.apply(ImmutableMap.of("instance", new AtomicReference<Instance>(instance), "zone", DEFAULT_ZONE_NAME, "email", identity, "userName", prefix)); + assertFalse(Strings.isNullOrEmpty(result), "Password shouldn't be empty"); + } + + @AfterClass(groups = { "integration", "live" }) + protected void tearDownContext() { + try { + waitOperationDone(api().delete(INSTANCE_NAME)); + waitOperationDone(diskApi().delete(DISK_NAME)); + waitOperationDone(api.networks().delete(INSTANCE_NETWORK_NAME)); + } catch (Exception e) { + // we don't really care about any exception here, so just delete away. + } + } +} http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/internal/BaseGoogleComputeEngineApiLiveTest.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/internal/BaseGoogleComputeEngineApiLiveTest.java b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/internal/BaseGoogleComputeEngineApiLiveTest.java index 41e1e74..086c3ee 100644 --- a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/internal/BaseGoogleComputeEngineApiLiveTest.java +++ b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/internal/BaseGoogleComputeEngineApiLiveTest.java @@ -60,6 +60,7 @@ public class BaseGoogleComputeEngineApiLiveTest extends BaseApiLiveTest<GoogleCo protected static final String TARGET_HTTP_PROXY_API_URL_SUFFIX = "/global/targetHttpProxies/"; protected static final String GOOGLE_PROJECT = "google"; + protected Injector injector; protected Predicate<AtomicReference<Operation>> operationDone; protected URI projectUrl; @@ -73,7 +74,7 @@ public class BaseGoogleComputeEngineApiLiveTest extends BaseApiLiveTest<GoogleCo } @Override protected GoogleComputeEngineApi create(Properties props, Iterable<Module> modules) { - Injector injector = newBuilder().modules(modules).overrides(props).buildInjector(); + injector = newBuilder().modules(modules).overrides(props).buildInjector(); operationDone = injector.getInstance(Key.get(new TypeLiteral<Predicate<AtomicReference<Operation>>>() { })); projectUrl = injector.getInstance(Key.get(new TypeLiteral<Supplier<URI>>() { http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/parse/ParseInstanceSerialOutputTest.java ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/parse/ParseInstanceSerialOutputTest.java b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/parse/ParseInstanceSerialOutputTest.java index 5b8eca5..eaf9708 100644 --- a/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/parse/ParseInstanceSerialOutputTest.java +++ b/google-compute-engine/src/test/java/org/jclouds/googlecomputeengine/parse/ParseInstanceSerialOutputTest.java @@ -45,4 +45,12 @@ public class ParseInstanceSerialOutputTest extends BaseGoogleComputeEngineParseT URI.create(baseUrl + "/party/zones/us-central1-a/instances/test-instance/serialPort"), "console output"); } + + @Consumes(APPLICATION_JSON) + public SerialPortOutput expected(String baseUrl, String contents) { + return SerialPortOutput.create( + URI.create(baseUrl + "/party/zones/us-central1-a/instances/test-instance/serialPort"), + contents + ); + } } http://git-wip-us.apache.org/repos/asf/jclouds-labs-google/blob/276ee07e/google-compute-engine/src/test/resources/instance_serial_port_4_windows.json ---------------------------------------------------------------------- diff --git a/google-compute-engine/src/test/resources/instance_serial_port_4_windows.json b/google-compute-engine/src/test/resources/instance_serial_port_4_windows.json new file mode 100644 index 0000000..f78df2c --- /dev/null +++ b/google-compute-engine/src/test/resources/instance_serial_port_4_windows.json @@ -0,0 +1,5 @@ +{ + "kind": "compute#serialPortOutput", + "selfLink": "https://www.googleapis.com/compute/v1/projects/party/zones/us-central1-a/instances/test-instance/serialPort", + "contents": "{\"ready\":true,\"version\":\"Microsoft Windows NT 6.1.7601 Service Pack 1\"}\n{\"encryptedPassword\":\"uiHDEhxyvj6lF5GalHh9TsMZb4bG6Y9qGmFb9S3XI29yvVsDCLdp4IbUg21MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5TxQo9hL0lBW7o3FRM\\/UIXCeRk39ObUl2AjDmQ0mcw1byJI5v9KVJnNMaHdRCy\\/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjSbvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXrfIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==\",\"exponent\":\"AQAB\",\"modulus\":\"0tiKdO2JmBHss26jnrSAwb583KG\\/ZIw5JwwMPXrCVsFAPwY1OV3RlT1Hp4Xvpibr7rvJbOC+f\\/Gd0cBrK5pccQfccB+OHKpbBof473zEfRbdtFwPn10RfAFj\\/xikW0r\\/XxgG\\/c8tz9bmALBStGqmwOVOLRHxjwgtGu4poeuwmFfG6TuwgCadxpllW74mviFd4LZVSuCSni5YJnBM2HSJ8NP6g1fqI17KDXt2XO\\/7kSItubmMk+HGEXdH4qiugHYewaIf1o4XSQROC8xlRl7t\\/RaD4U58hKYkVwg0Ir7WzYzAVpG2UR4Co\\/GDG9Hct7HOYekDqVQ+sSZbwzajnVunkw==\",\"passwordFound\":true,\"userName\":\"example-user\"}\n" +} \ No newline at end of file
