mirror of https://github.com/apache/jclouds.git
GCE ResetWindowsPassword
- Function which tells to GCE to reset Windows password - InstanceApiLiveTest for windows - Unit test for decrypting windows password - fixed compile errors
This commit is contained in:
parent
52de3b5766
commit
f98116ec83
|
@ -57,6 +57,11 @@
|
|||
<artifactId>jclouds-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.jclouds.driver</groupId>
|
||||
<artifactId>jclouds-bouncycastle</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.jclouds.common</groupId>
|
||||
<artifactId>googlecloud</artifactId>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -31,13 +31,16 @@ import java.net.URI;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
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.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.Atomics;
|
||||
|
@ -45,6 +48,7 @@ import com.google.common.util.concurrent.UncheckedTimeoutException;
|
|||
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;
|
||||
|
@ -90,6 +94,7 @@ public final class GoogleComputeEngineServiceAdapter
|
|||
private final Resources resources;
|
||||
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;
|
||||
private final LoadingCache<URI, Image> diskURIToImage;
|
||||
|
@ -97,6 +102,7 @@ public final class GoogleComputeEngineServiceAdapter
|
|||
@Inject GoogleComputeEngineServiceAdapter(JustProvider justProvider, GoogleComputeEngineApi api,
|
||||
Predicate<AtomicReference<Operation>> operationDone,
|
||||
Predicate<AtomicReference<Instance>> instanceVisible,
|
||||
Function<Map<String, ?>, String> windowsPasswordGenerator,
|
||||
Resources resources,
|
||||
FirewallTagNamingConvention.Factory firewallTagNamingConvention,
|
||||
@Named(IMAGE_PROJECTS) String imageProjects,
|
||||
|
@ -105,6 +111,7 @@ public final class GoogleComputeEngineServiceAdapter
|
|||
this.api = api;
|
||||
this.operationDone = operationDone;
|
||||
this.instanceVisible = instanceVisible;
|
||||
this.windowsPasswordGenerator = windowsPasswordGenerator;
|
||||
this.resources = resources;
|
||||
this.firewallTagNamingConvention = firewallTagNamingConvention;
|
||||
this.imageProjects = Splitter.on(',').omitEmptyStrings().splitToList(imageProjects);
|
||||
|
@ -187,6 +194,14 @@ public final class GoogleComputeEngineServiceAdapter
|
|||
// Add lookup for InstanceToNodeMetadata
|
||||
diskURIToImage.getUnchecked(instance.get().disks().get(0).source());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ 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.loaders.DiskURIToImage;
|
||||
import org.jclouds.googlecomputeengine.compute.options.GoogleComputeEngineTemplateOptions;
|
||||
import org.jclouds.googlecomputeengine.compute.predicates.AtomicInstanceVisible;
|
||||
|
@ -121,6 +122,9 @@ public final class GoogleComputeEngineServiceContextModule
|
|||
bind(new TypeLiteral<Function<String, OperatingSystem>>() {
|
||||
}).to(ImageNameToOperatingSystem.class);
|
||||
|
||||
bind(new TypeLiteral<Function<Map<String, ?>, String>>() {
|
||||
}).to(ResetWindowsPassword.class);
|
||||
|
||||
bind(FirewallTagNamingConvention.Factory.class).in(Scopes.SINGLETON);
|
||||
|
||||
bind(new TypeLiteral<CacheLoader<URI, Image>>() {
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* 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 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 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 org.jclouds.util.Predicates2;
|
||||
|
||||
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 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 static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import org.jclouds.scriptbuilder.domain.Statement;
|
|||
public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
|
||||
|
||||
private boolean autoCreateKeyPair = true;
|
||||
private boolean autoCreateWindowsPassword;
|
||||
private List<ServiceAccount> serviceAccounts;
|
||||
private String bootDiskType;
|
||||
private boolean preemptible = false;
|
||||
|
@ -46,6 +47,7 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
|
|||
GoogleComputeEngineTemplateOptions eTo = GoogleComputeEngineTemplateOptions.class.cast(to);
|
||||
eTo.autoCreateKeyPair(autoCreateKeyPair());
|
||||
eTo.serviceAccounts(serviceAccounts());
|
||||
eTo.autoCreateWindowsPassword(autoCreateWindowsPassword());
|
||||
eTo.bootDiskType(bootDiskType());
|
||||
eTo.preemptible(preemptible());
|
||||
}
|
||||
|
@ -107,6 +109,24 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
|
|||
return preemptible;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of service accounts, with their specified scopes, that will be authorize on created instances.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
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 javax.crypto.Cipher;
|
||||
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 static org.easymock.EasyMock.createMock;
|
||||
import static org.easymock.EasyMock.eq;
|
||||
import static org.easymock.EasyMock.expect;
|
||||
import static org.easymock.EasyMock.isA;
|
||||
import static org.easymock.EasyMock.replay;
|
||||
import static org.easymock.EasyMock.verify;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
@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", "test@google.com", "userName", "test"));
|
||||
|
||||
verify(api, instanceApi, operation, serialPortOutput);
|
||||
|
||||
assertEquals(result, password);
|
||||
}
|
||||
}
|
|
@ -67,6 +67,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"));
|
||||
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
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 java.net.URI;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.jclouds.googlecomputeengine.options.ListOptions.Builder.filter;
|
||||
import static org.testng.Assert.assertFalse;
|
||||
|
||||
@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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,6 +63,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 long operationDoneInterval;
|
||||
protected long operationDoneTimeout;
|
||||
|
@ -78,7 +79,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>>() {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue