diff --git a/src/main/java/org/jenkinsci/plugins/vsphere/builders/Clone.java b/src/main/java/org/jenkinsci/plugins/vsphere/builders/Clone.java index dcde28df..96aca354 100644 --- a/src/main/java/org/jenkinsci/plugins/vsphere/builders/Clone.java +++ b/src/main/java/org/jenkinsci/plugins/vsphere/builders/Clone.java @@ -28,6 +28,8 @@ import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; import edu.umd.cs.findbugs.annotations.NonNull; @@ -60,20 +62,44 @@ public class Clone extends VSphereBuildStep { private final Integer timeoutInSeconds; private String IP; + /** Optionally used by {@code #linkedClone} setting or on its own, + * conflicts with {@code #namedSnapshot}. Is {@code null} by default. */ + private final Boolean useCurrentSnapshot; + /** Optionally used by {@code #linkedClone} setting or on its own, + * conflicts with {@code #useCurrentSnapshot}. Is {@code null} by default. */ + private final String namedSnapshot; + private final Map extraConfigParameters; + @DataBoundConstructor public Clone(String sourceName, String clone, boolean linkedClone, String resourcePool, String cluster, String datastore, String folder, - boolean powerOn, Integer timeoutInSeconds, String customizationSpec) throws VSphereException { + boolean powerOn, Integer timeoutInSeconds, String customizationSpec, + Boolean useCurrentSnapshot, String namedSnapshot, + Map extraConfigParameters) throws VSphereException { this.sourceName = sourceName; this.clone = clone; this.linkedClone = linkedClone; - this.resourcePool=resourcePool; - this.cluster=cluster; - this.datastore=datastore; - this.folder=folder; - this.customizationSpec=customizationSpec; - this.powerOn=powerOn; + this.resourcePool = resourcePool; + this.cluster = cluster; + this.datastore = datastore; + this.folder = folder; + this.customizationSpec = customizationSpec; + this.powerOn = powerOn; this.timeoutInSeconds = timeoutInSeconds; + this.useCurrentSnapshot = useCurrentSnapshot; + + // Config form data may involve empty strings - treat them as null + if (namedSnapshot == null || namedSnapshot.isEmpty()) { + this.namedSnapshot = null; + } else { + this.namedSnapshot = namedSnapshot; + } + + if (extraConfigParameters == null || extraConfigParameters.isEmpty()) { + this.extraConfigParameters = null; + } else { + this.extraConfigParameters = extraConfigParameters; + } } public String getSourceName() { @@ -88,6 +114,27 @@ public boolean isLinkedClone() { return linkedClone; } + public String getNamedSnapshot() { + return namedSnapshot; + } + + public boolean isUseCurrentSnapshot() { + if (useCurrentSnapshot == null) { + if (namedSnapshot == null) { + // Hard-coded default in VSphere.cloneVm() + // TOTHINK: Should this rely on linkedClone value? + return true; + } + + // Will use specified named snapshot + return false; + } + + // Caller had an explicit request + // Note that if linkedClone==true, at least some snapshot must be used + return useCurrentSnapshot; + } + public String getCluster() { return cluster; } @@ -119,6 +166,10 @@ public int getTimeoutInSeconds() { return timeoutInSeconds.intValue(); } + public Map getExtraConfigParameters() { + return extraConfigParameters; + } + @Override public void perform(@NonNull Run run, @NonNull FilePath filePath, @NonNull Launcher launcher, @NonNull TaskListener listener) throws InterruptedException, IOException { try { @@ -163,6 +214,8 @@ private boolean cloneFromSource(final Run run, final Launcher launcher, fi String expandedFolder = folder; String expandedResourcePool = resourcePool; String expandedCustomizationSpec = customizationSpec; + String expandedNamedSnapshot = namedSnapshot; + Map expandedExtraConfigParameters; EnvVars env; try { env = run.getEnvironment(listener); @@ -179,9 +232,29 @@ private boolean cloneFromSource(final Run run, final Launcher launcher, fi expandedFolder = env.expand(folder); expandedResourcePool = env.expand(resourcePool); expandedCustomizationSpec = env.expand(customizationSpec); + if (namedSnapshot != null) { + expandedNamedSnapshot = env.expand(namedSnapshot); + } } - vsphere.cloneVm(expandedClone, expandedSource, linkedClone, expandedResourcePool, expandedCluster, - expandedDatastore, expandedFolder, powerOn, expandedCustomizationSpec, jLogger); + + if (extraConfigParameters != null && !(extraConfigParameters.isEmpty())) { + // Always pass a copy of the non-trivial original parameter map + // (expanded or not), just in case, to protect caller's data. + expandedExtraConfigParameters = new HashMap(); + if (run instanceof AbstractBuild) { + extraConfigParameters.forEach((k, v) -> expandedExtraConfigParameters.put(k, env.expand(v))); + } else { + expandedExtraConfigParameters.putAll(extraConfigParameters); + } + } else { + // Only init to null here, due to lambda used in forEach() above + expandedExtraConfigParameters = null; + } + + vsphere.cloneOrDeployVm(expandedClone, expandedSource, linkedClone, expandedResourcePool, expandedCluster, + expandedDatastore, expandedFolder, this.isUseCurrentSnapshot(), expandedNamedSnapshot, + powerOn, expandedExtraConfigParameters, expandedCustomizationSpec, jLogger); + final int timeoutInSecondsForGetIp = getTimeoutInSeconds(); if (powerOn && timeoutInSecondsForGetIp>0) { VSphereLogger.vsLogger(jLogger, "Powering on VM \""+expandedClone+"\". Waiting for its IP for the next "+timeoutInSecondsForGetIp+" seconds."); @@ -264,7 +337,11 @@ public FormValidation doTestData(@AncestorInPath Item context, @QueryParameter String serverName, @QueryParameter String sourceName, @QueryParameter String clone, @QueryParameter String resourcePool, @QueryParameter String cluster, - @QueryParameter String customizationSpec) { + @QueryParameter String customizationSpec, + @QueryParameter Boolean linkedClone, + @QueryParameter Boolean useCurrentSnapshot, + @QueryParameter String namedSnapshot) { + // TODO? @QueryParameter Map extraConfigParameters throwUnlessUserHasPermissionToConfigureJob(context); try { if (sourceName.length() == 0 || clone.length()==0 || serverName.length()==0 @@ -285,9 +362,21 @@ public FormValidation doTestData(@AncestorInPath Item context, if (vm == null) return FormValidation.error(Messages.validation_notFound("sourceName")); - VirtualMachineSnapshot snap = vm.getCurrentSnapShot(); - if (snap == null) - return FormValidation.error(Messages.validation_noSnapshots()); + if (linkedClone || useCurrentSnapshot || (namedSnapshot != null && !(namedSnapshot.isEmpty()))) { + // Use-case (according to parameters) requires a snapshot + VirtualMachineSnapshot snap; + if (namedSnapshot == null || namedSnapshot.isEmpty()) { + // either useCurrentSnapshot or linkedClone is true + snap = vm.getCurrentSnapShot(); + } else { + // namedSnapshot is non-trivial + if (useCurrentSnapshot) + return FormValidation.error(Messages.validation_useCurrentAndNamedSnapshots()); + snap = vsphere.getSnapshotInTree(vm, namedSnapshot); + } + if (snap == null) + return FormValidation.error(Messages.validation_noSnapshots()); + } if(customizationSpec != null && customizationSpec.length() > 0 && vsphere.getCustomizationSpecByName(customizationSpec) == null) { diff --git a/src/main/java/org/jenkinsci/plugins/vsphere/tools/VSphere.java b/src/main/java/org/jenkinsci/plugins/vsphere/tools/VSphere.java index 8481e1ed..0eb3c411 100644 --- a/src/main/java/org/jenkinsci/plugins/vsphere/tools/VSphere.java +++ b/src/main/java/org/jenkinsci/plugins/vsphere/tools/VSphere.java @@ -147,7 +147,6 @@ public void deployVm(String cloneName, String sourceName, boolean linkedClone, S final boolean useCurrentSnapshotIsFALSE = false; final String namedSnapshotIsNULL = null; final Map extraConfigParameters = null; - logMessage(jLogger, "Deploying new vm \""+ cloneName + "\" from template \""+sourceName+"\""); cloneOrDeployVm(cloneName, sourceName, linkedClone, resourcePoolName, cluster, datastoreName, folderName, useCurrentSnapshotIsFALSE, namedSnapshotIsNULL, powerOn, extraConfigParameters, customizationSpec, jLogger); } @@ -170,7 +169,6 @@ public void cloneVm(String cloneName, String sourceName, boolean linkedClone, St final boolean useCurrentSnapshotIsTRUE = true; final String namedSnapshotIsNULL = null; final Map extraConfigParameters = null; - logMessage(jLogger, "Creating a " + (linkedClone?"shallow":"deep") + " clone of \"" + sourceName + "\" to \"" + cloneName + "\""); cloneOrDeployVm(cloneName, sourceName, linkedClone, resourcePoolName, cluster, datastoreName, folderName, useCurrentSnapshotIsTRUE, namedSnapshotIsNULL, powerOn, extraConfigParameters, customizationSpec, jLogger); } @@ -220,6 +218,21 @@ public void cloneVm(String cloneName, String sourceName, boolean linkedClone, St * if anything goes wrong. */ public void cloneOrDeployVm(String cloneName, String sourceName, boolean linkedClone, String resourcePoolName, String cluster, String datastoreName, String folderName, boolean useCurrentSnapshot, final String namedSnapshot, boolean powerOn, Map extraConfigParameters, String customizationSpec, PrintStream jLogger) throws VSphereException { + if (namedSnapshot == null && extraConfigParameters == null) { + // NOTE: This "if" clause may be superfluous - just that previously + // this message was only logged by cloneVm() or deployVm()... so for + // least surprise and unexpected noise in the logs, effectively kept + // so for upgraded plugins where we can also directly call this method + // as a "buildStep" under a "vSphere" pipeline step. + if (useCurrentSnapshot) { + // Called from cloneVm() above. + logMessage(jLogger, "Creating a " + (linkedClone ? "shallow" : "deep") + " clone of \"" + sourceName + "\" to \"" + cloneName + "\""); + } else { + // Called from deployVm() above. + logMessage(jLogger, "Deploying new vm \""+ cloneName + "\" from template \""+sourceName+"\""); + } + } + try { final VirtualMachine sourceVm = getVmByName(sourceName); if (sourceVm==null) { @@ -239,11 +252,11 @@ public void cloneOrDeployVm(String cloneName, String sourceName, boolean linkedC if (namedSnapshot != null && !namedSnapshot.isEmpty()) { if (useCurrentSnapshot) { - throw new IllegalArgumentException("It is not valid to request a clone of " + sourceType + " \"" + sourceName + "\" based on its snapshot \"" + namedSnapshot + "\" AND also specify that the latest snapshot should be used. Either choose to use the latest snapshot, or name a snapshot, or neither, but not both."); + throw new IllegalArgumentException("It is not valid to request a clone of " + sourceType + " \"" + sourceName + "\" based on its snapshot \"" + namedSnapshot + "\" AND also specify that the latest snapshot should be used. Either choose to use the latest snapshot, or name a snapshot, or neither, but not both."); } final VirtualMachineSnapshot namedVMSnapshot = getSnapshotInTree(sourceVm, namedSnapshot); if (namedVMSnapshot == null) { - throw new VSphereNotFoundException("Snapshot", namedSnapshot, "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()); @@ -251,7 +264,7 @@ public void cloneOrDeployVm(String cloneName, String sourceName, boolean linkedC if (useCurrentSnapshot) { final VirtualMachineSnapshot currentSnapShot = sourceVm.getCurrentSnapShot(); if (currentSnapShot==null) { - throw new VSphereNotFoundException("Snapshot", null, "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()); diff --git a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/config.jelly b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/config.jelly index 20f531e6..55334206 100644 --- a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/config.jelly @@ -28,6 +28,14 @@ limitations under the License. + + + + + + + + @@ -54,5 +62,7 @@ limitations under the License. + + diff --git a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-extraConfigParameters.html b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-extraConfigParameters.html new file mode 100644 index 00000000..463c573e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-extraConfigParameters.html @@ -0,0 +1,9 @@ +
+ (Optional) A Map of parameters to set in the 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. + e.g. a variable named "guestinfo.Foo" with value "Bar" could + be read on the guest using the command-line + vmtoolsd --cmd "info-get guestinfo.Foo". +
diff --git a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-namedSnapshot.html b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-namedSnapshot.html new file mode 100644 index 00000000..ea39e01f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-namedSnapshot.html @@ -0,0 +1,6 @@ +
+ If set then the clone will be created from the source VM's + snapshot of this name. + If this is set then useCurrentSnapshot must not be set. + May impact the linkedClone behavior. +
diff --git a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-useCurrentSnapshot.html b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-useCurrentSnapshot.html new file mode 100644 index 00000000..eb4f7314 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Clone/help-useCurrentSnapshot.html @@ -0,0 +1,7 @@ +
+ If true then the clone will be created from the source VM's + "current" snapshot. This means that the VM must have + at least one snapshot. + If this is set then namedSnapshot must not be set. + May impact the linkedClone behavior. +
diff --git a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Messages.properties b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Messages.properties index c4f7e8de..89d8fa5d 100644 --- a/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/vsphere/builders/Messages.properties @@ -33,6 +33,7 @@ validation.alreadySet=Specified {0} is already a {1}! validation.exists=Specified {0} already exists! validation.notActually=Specified {0} is not actually a {0}! validation.noSnapshots=No snapshots found for specified template! +validation.useCurrentAndNamedSnapshots=Can not specify use of both a named and a current snapshot! validation.positiveInteger={0} must be a positive integer! validation.maxValue=Please enter a value less than {0}! validation.success=Success