Skip to content

Commit

Permalink
Merge pull request #73 from pjdarton/reconnect_to_existing_vm
Browse files Browse the repository at this point in the history
[JENKINS-44796] Recognize VMs that our Jenkins created and permit reconnection.
  • Loading branch information
pjdarton committed Jul 28, 2017
2 parents 0cb3277 + 235aca3 commit f87d1de
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 41 deletions.
6 changes: 3 additions & 3 deletions src/main/java/org/jenkinsci/plugins/vSphereCloud.java
Expand Up @@ -415,7 +415,7 @@ public static VSpherePlannedNode createInstance(final CloudProvisioningState tem
final Callable<Node> provisionNodeCallable = new Callable<Node>() {
public Node call() throws Exception {
try {
final Node newNode = provisionNewNode(templateState, whatWeShouldSpinUp, nodeName);
final Node newNode = provisionNewNode(whatWeShouldSpinUp, nodeName);
VSLOG.log(Level.INFO, "Provisioned new slave " + nodeName);
synchronized (templateState) {
templateState.provisionedSlaveNowActive(whatWeShouldSpinUp, nodeName);
Expand All @@ -436,10 +436,10 @@ public Node call() throws Exception {
return result;
}

private static Node provisionNewNode(final CloudProvisioningState algorithm, final CloudProvisioningRecord whatWeShouldSpinUp, final String cloneName)
private static Node provisionNewNode(final CloudProvisioningRecord whatWeShouldSpinUp, final String cloneName)
throws VSphereException, FormException, IOException, InterruptedException {
final vSphereCloudSlaveTemplate template = whatWeShouldSpinUp.getTemplate();
final vSphereCloudProvisionedSlave slave = template.provision(algorithm, cloneName, StreamTaskListener.fromStdout());
final vSphereCloudProvisionedSlave slave = template.provision(cloneName, StreamTaskListener.fromStdout());
// ensure Jenkins knows about us before we forget what we're doing,
// otherwise it'll just ask for more.
Jenkins.getInstance().addNode(slave);
Expand Down
80 changes: 67 additions & 13 deletions src/main/java/org/jenkinsci/plugins/vSphereCloudSlaveTemplate.java
Expand Up @@ -44,6 +44,7 @@
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -61,6 +62,7 @@
import org.jenkinsci.plugins.vsphere.builders.Messages;
import org.jenkinsci.plugins.vsphere.tools.CloudProvisioningState;
import org.jenkinsci.plugins.vsphere.tools.VSphere;
import org.jenkinsci.plugins.vsphere.tools.VSphereDuplicateException;
import org.jenkinsci.plugins.vsphere.tools.VSphereException;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
Expand All @@ -69,6 +71,8 @@
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel;
import com.cloudbees.plugins.credentials.domains.SchemeRequirement;
import com.vmware.vim25.OptionValue;
import com.vmware.vim25.VirtualMachineConfigInfo;
import com.vmware.vim25.mo.VirtualMachine;

/**
Expand All @@ -77,6 +81,8 @@
*/
public class vSphereCloudSlaveTemplate implements Describable<vSphereCloudSlaveTemplate> {
private static final Logger LOGGER = Logger.getLogger(vSphereCloudSlaveTemplate.class.getName());
private static final String VSPHERE_ATTR_FOR_JENKINSURL = vSphereCloudSlaveTemplate.class.getSimpleName()
+ ".jenkinsUrl";

protected static final SchemeRequirement HTTP_SCHEME = new SchemeRequirement("http");
protected static final SchemeRequirement HTTPS_SCHEME = new SchemeRequirement("https");
Expand Down Expand Up @@ -351,7 +357,7 @@ protected Object readResolve() {
return this;
}

public vSphereCloudProvisionedSlave provision(final CloudProvisioningState algorithm, final String cloneName, final TaskListener listener) throws VSphereException, FormException, IOException, InterruptedException {
public vSphereCloudProvisionedSlave provision(final String cloneName, final TaskListener listener) throws VSphereException, FormException, IOException, InterruptedException {
vSphereCloudProvisionedSlave slave = null;
final PrintStream logger = listener.getLogger();
final VSphere vSphere = getParent().vSphereInstance();
Expand All @@ -371,14 +377,28 @@ public vSphereCloudProvisionedSlave provision(final CloudProvisioningState algor
useCurrentSnapshot = false;
snapshotToUse = null;
}
vSphere.cloneOrDeployVm(cloneName, this.masterImageName, this.linkedClone, this.resourcePool, this.cluster, this.datastore, this.folder, useCurrentSnapshot, snapshotToUse, POWER_ON, this.customizationSpec, logger);
try {
if( this.guestInfoProperties!=null && !this.guestInfoProperties.isEmpty()) {
final Map<String, String> resolvedGuestInfoProperties = calculateGuestInfoProperties(cloneName, listener);
if( !resolvedGuestInfoProperties.isEmpty() ) {
LOGGER.log(Level.FINE, "Provisioning slave {0} with guestinfo properties {1}", new Object[]{ cloneName, resolvedGuestInfoProperties });
vSphere.addGuestInfoVariable(cloneName, resolvedGuestInfoProperties);
}
vSphere.cloneOrDeployVm(cloneName, this.masterImageName, this.linkedClone, this.resourcePool, this.cluster, this.datastore, this.folder, useCurrentSnapshot, snapshotToUse, POWER_ON, this.customizationSpec, logger);
LOGGER.log(Level.FINE, "Created new VM {0} from image {1}", new Object[]{ cloneName, this.masterImageName });
} catch (VSphereDuplicateException ex) {
final String vmJenkinsUrl = findWhichJenkinsThisVMBelongsTo(vSphere, cloneName);
if ( vmJenkinsUrl==null ) {
LOGGER.log(Level.SEVERE, "VM {0} name clashes with one we wanted to use, but it wasn't started by this plugin.", cloneName );
throw ex;
}
final String ourJenkinsUrl = Jenkins.getActiveInstance().getRootUrl();
if ( vmJenkinsUrl.equals(ourJenkinsUrl) ) {
LOGGER.log(Level.INFO, "Found existing VM {0} that we started previously (and must have either lost track of it or failed to delete it).", cloneName );
} else {
LOGGER.log(Level.SEVERE, "VM {0} name clashes with one we wanted to use, but it doesn't belong to this Jenkins server: it belongs to {1}. You MUST reconfigure one of these Jenkins servers to use a different naming strategy so that we no longer get clashes within vSphere host {2}. i.e. change the cloneNamePrefix on one/both to ensure uniqueness.", new Object[]{ cloneName, vmJenkinsUrl, this.getParent().getVsHost() } );
throw ex;
}
}
try {
final Map<String, String> resolvedExtraConfigParameters = calculateExtraConfigParameters(cloneName, listener);
if( !resolvedExtraConfigParameters.isEmpty() ) {
LOGGER.log(Level.FINE, "Provisioning slave {0} with guestinfo properties {1}", new Object[]{ cloneName, resolvedExtraConfigParameters });
vSphere.setExtraConfigParameters(cloneName, resolvedExtraConfigParameters);
}
final ComputerLauncher configuredLauncher = determineLauncher(vSphere, cloneName);
final RetentionStrategy<?> configuredStrategy = determineRetention();
Expand Down Expand Up @@ -536,17 +556,51 @@ public static List<Descriptor<RetentionStrategy<?>>> getRetentionStrategyDescrip
}
}

private Map<String, String> calculateGuestInfoProperties(final String cloneName, final TaskListener listener)
private static String findWhichJenkinsThisVMBelongsTo(final VSphere vSphere, String cloneName) {
final VirtualMachine vm;
try {
vm = vSphere.getVmByName(cloneName);
} catch (VSphereException e) {
return null;
}
final VirtualMachineConfigInfo config = vm.getConfig();
if (config == null) {
return null;
}
final OptionValue[] extraConfigs = config.extraConfig;
if (extraConfigs == null) {
return null;
}
String vmJenkinsUrl = null;
for (final OptionValue ec : extraConfigs) {
final String configName = ec.key;
final String configValue = ec.value == null ? null : ec.value.toString();
if (VSPHERE_ATTR_FOR_JENKINSURL.equals(configName)) {
vmJenkinsUrl = configValue;
}
}
return vmJenkinsUrl;
}

private Map<String, String> calculateExtraConfigParameters(final String cloneName, final TaskListener listener)
throws IOException, InterruptedException {
final EnvVars knownVariables = calculateVariablesForGuestInfo(cloneName, listener);
final Map<String, String> resolvedGuestInfoProperties = new LinkedHashMap<String, String>();
for( final VSphereGuestInfoProperty property : this.guestInfoProperties ) {
final Map<String, String> result = new LinkedHashMap<String, String>();
final String jenkinsUrl = Jenkins.getActiveInstance().getRootUrl();
if (jenkinsUrl != null) {
result.put(VSPHERE_ATTR_FOR_JENKINSURL, jenkinsUrl);
}
List<? extends VSphereGuestInfoProperty> guestInfoConfig = this.guestInfoProperties;
if (guestInfoConfig == null) {
guestInfoConfig = Collections.emptyList();
}
for (final VSphereGuestInfoProperty property : guestInfoConfig) {
final String name = property.getName();
final String configuredValue = property.getValue();
final String resolvedValue = Util.replaceMacro(configuredValue, knownVariables);
resolvedGuestInfoProperties.put(name, resolvedValue);
result.put("guestinfo." + name, resolvedValue);
}
return resolvedGuestInfoProperties;
return result;
}

private EnvVars calculateVariablesForGuestInfo(final String cloneName, final TaskListener listener)
Expand Down
53 changes: 28 additions & 25 deletions src/main/java/org/jenkinsci/plugins/vsphere/tools/VSphere.java
Expand Up @@ -195,10 +195,10 @@ public void cloneOrDeployVm(String cloneName, String sourceName, boolean linkedC
try{
final VirtualMachine sourceVm = getVmByName(sourceName);
if(sourceVm==null) {
throw new VSphereException("VM or template \"" + sourceName + "\" not found");
throw new VSphereNotFoundException("VM or template", sourceName);
}
if(getVmByName(cloneName)!=null){
throw new VSphereException("VM \"" + cloneName + "\" already exists");
throw new VSphereDuplicateException("VM", cloneName);
}

final VirtualMachineConfigInfo vmConfig = sourceVm.getConfig();
Expand All @@ -215,15 +215,15 @@ public void cloneOrDeployVm(String cloneName, String sourceName, boolean linkedC
}
final VirtualMachineSnapshot namedVMSnapshot = getSnapshotInTree(sourceVm, namedSnapshot);
if (namedVMSnapshot == null) {
throw new VSphereException("Source " + sourceType + " \"" + sourceName + "\" has no snapshot called \"" + namedSnapshot + "\".");
throw new VSphereNotFoundException("Snapshot", namedSnapshot, "Source " + sourceType + " \"" + sourceName + "\" has no snapshot called \"" + namedSnapshot + "\".");
}
logMessage(jLogger, "Clone of " + sourceType + " \"" + sourceName + "\" will be based on named snapshot \"" + namedSnapshot + "\".");
cloneSpec.setSnapshot(namedVMSnapshot.getMOR());
}
if (useCurrentSnapshot) {
final VirtualMachineSnapshot currentSnapShot = sourceVm.getCurrentSnapShot();
if(currentSnapShot==null){
throw new VSphereException("Source " + sourceType + " \"" + sourceName + "\" requires at least one snapshot.");
throw new VSphereNotFoundException("Snapshot", null, "Source " + sourceType + " \"" + sourceName + "\" requires at least one snapshot.");
}
logMessage(jLogger, "Clone of " + sourceType + " \"" + sourceName + "\" will be based on current snapshot \"" + currentSnapShot.toString() + "\".");
cloneSpec.setSnapshot(currentSnapShot.getMOR());
Expand Down Expand Up @@ -292,7 +292,7 @@ private VirtualMachineRelocateSpec createRelocateSpec(PrintStream jLogger, boole
ResourcePool resourcePool = getResourcePoolByName(resourcePoolName, clusterResource);

if (resourcePool == null) {
throw new VSphereException("Resource pool \"" + resourcePoolName + "\" not found");
throw new VSphereNotFoundException("Resource pool", resourcePoolName);
}

rel.setPool(resourcePool.getMOR());
Expand All @@ -303,7 +303,7 @@ private VirtualMachineRelocateSpec createRelocateSpec(PrintStream jLogger, boole
if (datastoreName != null && !datastoreName.isEmpty()) {
Datastore datastore = getDatastoreByName(datastoreName, clusterResource);
if (datastore==null){
throw new VSphereException("Datastore \"" + datastoreName + "\" not found!");
throw new VSphereNotFoundException("Datastore", datastoreName);
}
rel.setDatastore(datastore.getMOR());
}
Expand All @@ -313,7 +313,7 @@ private VirtualMachineRelocateSpec createRelocateSpec(PrintStream jLogger, boole
public void reconfigureVm(String name, VirtualMachineConfigSpec spec) throws VSphereException {
VirtualMachine vm = getVmByName(name);
if(vm==null) {
throw new VSphereException("No VM or template " + name + " found");
throw new VSphereNotFoundException("VM or template", name);
}
LOGGER.log(Level.FINER, "Reconfiguring VM. Please wait ...");
try {
Expand All @@ -339,7 +339,7 @@ public void startVm(String name, int timeoutInSeconds) throws VSphereException {
try{
VirtualMachine vm = getVmByName(name);
if (vm == null) {
throw new VSphereException("Vm " + name + " was not found");
throw new VSphereNotFoundException("VM", name);
}
if(isPoweredOn(vm))
return;
Expand Down Expand Up @@ -436,7 +436,7 @@ public void revertToSnapshot(String vmName, String snapName) throws VSphereExcep

if (snap == null) {
LOGGER.log(Level.SEVERE, "Cannot find snapshot: '" + snapName + "' for virtual machine: '" + vm.getName()+"'");
throw new VSphereException("Virtual Machine snapshot cannot be found");
throw new VSphereNotFoundException("Snapshot", snapName);
}

try{
Expand All @@ -459,7 +459,7 @@ public void deleteSnapshot(String vmName, String snapName, boolean consolidate,
VirtualMachineSnapshot snap = getSnapshotInTree(vm, snapName);

if (snap == null && failOnNoExist) {
throw new VSphereException("Virtual Machine snapshot cannot be found");
throw new VSphereNotFoundException("Snapshot", snapName);
}

try{
Expand Down Expand Up @@ -494,7 +494,7 @@ public void takeSnapshot(String vmName, String snapshot, String description, boo
final String message = "Could not take snapshot";
VirtualMachine vmToSnapshot = getVmByName(vmName);
if (vmToSnapshot == null) {
throw new VSphereException("Vm " + vmName + " was not found");
throw new VSphereNotFoundException("VM", vmName);
}
try {
Task task = vmToSnapshot.createSnapshot_Task(snapshot, description, snapMemory, !snapMemory);
Expand Down Expand Up @@ -771,16 +771,16 @@ private ClusterComputeResource getClusterByName(final String clusterName) throws
}

/**
* Detroys the VM in vSphere
* Destroys the VM in vSphere
* @param name - VM object to destroy
* @param failOnNoExist If true and the VM does not exist then a VSphereException will be thrown.
* @param failOnNoExist If true and the VM does not exist then a {@link VSphereNotFoundException} will be thrown.
* @throws VSphereException If an error occurred.
*/
public void destroyVm(String name, boolean failOnNoExist) throws VSphereException{
try{
VirtualMachine vm = getVmByName(name);
if(vm==null){
if(failOnNoExist) throw new VSphereException("VM \"" + name + "\" does not exist");
if (failOnNoExist) throw new VSphereNotFoundException("VM", name);

LOGGER.log(Level.FINER, "VM \"" + name + "\" does not exist, or already deleted!");
return;
Expand Down Expand Up @@ -819,7 +819,7 @@ public void renameVmSnapshot(String vmName, String oldName, String newName, Stri
try{
VirtualMachine vm = getVmByName(vmName);
if(vm==null){
throw new VSphereException("VM \"" + vmName + "\" does not exist");
throw new VSphereNotFoundException("VM", vmName);
}

VirtualMachineSnapshot snapshot = getSnapshotInTree(vm, oldName);
Expand All @@ -846,7 +846,7 @@ public void renameVm(String oldName, String newName) throws VSphereException{
try{
VirtualMachine vm = getVmByName(oldName);
if(vm==null){
throw new VSphereException("VM \"" + oldName + "\" does not exist");
throw new VSphereNotFoundException("VM", oldName);
}

final Task task = vm.rename_Task(newName);
Expand Down Expand Up @@ -1055,27 +1055,30 @@ public DistributedVirtualSwitch getDistributedVirtualSwitchByPortGroup(
}

/**
* Passes data to a VM's "guestinfo" object. This data can then be read by
* the VMware Tools on the guest.
* Passes data to a VM's "extra config" object. This data can then be read
* back at a later stage.
* In the case of parameters whose name starts "guestinfo.", the parameter
* can be read by the VMware Tools on the client OS.
* <p>
* e.g. a variable named "Foo" with value "Bar" could be read on the guest
* using the command-line <tt>vmtoolsd --cmd "info-get guestinfo.Foo"</tt>.
* e.g. a variable named "guestinfo.Foo" with value "Bar" could be read on
* the guest using the command-line
* <tt>vmtoolsd --cmd "info-get guestinfo.Foo"</tt>.
* </p>
*
* @param vmName
* The name of the VM.
* @param variables
* @param parameters
* A {@link Map} of variable name to variable value.
* @throws VSphereException
* If an error occurred.
*/
public void addGuestInfoVariable(String vmName, Map<String, String> variables) throws VSphereException {
public void setExtraConfigParameters(String vmName, Map<String, String> parameters) throws VSphereException {
VirtualMachineConfigSpec cs = new VirtualMachineConfigSpec();
OptionValue[] ourOptionValues = new OptionValue[variables.size()];
OptionValue[] ourOptionValues = new OptionValue[parameters.size()];
List<OptionValue> optionValues = new ArrayList<>();
for (Map.Entry<String, String> eachVariable : variables.entrySet()) {
for (Map.Entry<String, String> eachVariable : parameters.entrySet()) {
OptionValue ov = new OptionValue();
ov.setKey("guestinfo." + eachVariable.getKey());
ov.setKey(eachVariable.getKey());
ov.setValue(eachVariable.getValue());
optionValues.add(ov);
}
Expand Down
@@ -0,0 +1,16 @@
package org.jenkinsci.plugins.vsphere.tools;

public class VSphereDuplicateException extends VSphereException {
private final String resourceType;
private final String resourceName;

public VSphereDuplicateException(String resourceType, String resourceName) {
this(resourceType, resourceName, null);
}

public VSphereDuplicateException(String resourceType, String resourceName, Throwable cause) {
super(resourceType + " \"" + resourceName + "\" already exists", cause);
this.resourceType = resourceType;
this.resourceName = resourceName;
}
}

0 comments on commit f87d1de

Please sign in to comment.