2828import java .io .PrintStream ;
2929import java .io .PrintWriter ;
3030import java .io .StringWriter ;
31+ import java .util .HashMap ;
32+ import java .util .Map ;
3133
3234import edu .umd .cs .findbugs .annotations .NonNull ;
3335
@@ -60,20 +62,44 @@ public class Clone extends VSphereBuildStep {
6062 private final Integer timeoutInSeconds ;
6163 private String IP ;
6264
65+ /** Optionally used by {@code #linkedClone} setting or on its own,
66+ * conflicts with {@code #namedSnapshot}. Is {@code null} by default. */
67+ private final Boolean useCurrentSnapshot ;
68+ /** Optionally used by {@code #linkedClone} setting or on its own,
69+ * conflicts with {@code #useCurrentSnapshot}. Is {@code null} by default. */
70+ private final String namedSnapshot ;
71+ private final Map <String , String > extraConfigParameters ;
72+
6373 @ DataBoundConstructor
6474 public Clone (String sourceName , String clone , boolean linkedClone ,
6575 String resourcePool , String cluster , String datastore , String folder ,
66- boolean powerOn , Integer timeoutInSeconds , String customizationSpec ) throws VSphereException {
76+ boolean powerOn , Integer timeoutInSeconds , String customizationSpec ,
77+ Boolean useCurrentSnapshot , String namedSnapshot ,
78+ Map <String , String > extraConfigParameters ) throws VSphereException {
6779 this .sourceName = sourceName ;
6880 this .clone = clone ;
6981 this .linkedClone = linkedClone ;
70- this .resourcePool = resourcePool ;
71- this .cluster = cluster ;
72- this .datastore = datastore ;
73- this .folder = folder ;
74- this .customizationSpec = customizationSpec ;
75- this .powerOn = powerOn ;
82+ this .resourcePool = resourcePool ;
83+ this .cluster = cluster ;
84+ this .datastore = datastore ;
85+ this .folder = folder ;
86+ this .customizationSpec = customizationSpec ;
87+ this .powerOn = powerOn ;
7688 this .timeoutInSeconds = timeoutInSeconds ;
89+ this .useCurrentSnapshot = useCurrentSnapshot ;
90+
91+ // Config form data may involve empty strings - treat them as null
92+ if (namedSnapshot == null || namedSnapshot .isEmpty ()) {
93+ this .namedSnapshot = null ;
94+ } else {
95+ this .namedSnapshot = namedSnapshot ;
96+ }
97+
98+ if (extraConfigParameters == null || extraConfigParameters .isEmpty ()) {
99+ this .extraConfigParameters = null ;
100+ } else {
101+ this .extraConfigParameters = extraConfigParameters ;
102+ }
77103 }
78104
79105 public String getSourceName () {
@@ -88,6 +114,27 @@ public boolean isLinkedClone() {
88114 return linkedClone ;
89115 }
90116
117+ public String getNamedSnapshot () {
118+ return namedSnapshot ;
119+ }
120+
121+ public boolean isUseCurrentSnapshot () {
122+ if (useCurrentSnapshot == null ) {
123+ if (namedSnapshot == null ) {
124+ // Hard-coded default in VSphere.cloneVm()
125+ // TOTHINK: Should this rely on linkedClone value?
126+ return true ;
127+ }
128+
129+ // Will use specified named snapshot
130+ return false ;
131+ }
132+
133+ // Caller had an explicit request
134+ // Note that if linkedClone==true, at least some snapshot must be used
135+ return useCurrentSnapshot ;
136+ }
137+
91138 public String getCluster () {
92139 return cluster ;
93140 }
@@ -119,6 +166,10 @@ public int getTimeoutInSeconds() {
119166 return timeoutInSeconds .intValue ();
120167 }
121168
169+ public Map <String , String > getExtraConfigParameters () {
170+ return extraConfigParameters ;
171+ }
172+
122173 @ Override
123174 public void perform (@ NonNull Run <?, ?> run , @ NonNull FilePath filePath , @ NonNull Launcher launcher , @ NonNull TaskListener listener ) throws InterruptedException , IOException {
124175 try {
@@ -163,6 +214,8 @@ private boolean cloneFromSource(final Run<?, ?> run, final Launcher launcher, fi
163214 String expandedFolder = folder ;
164215 String expandedResourcePool = resourcePool ;
165216 String expandedCustomizationSpec = customizationSpec ;
217+ String expandedNamedSnapshot = namedSnapshot ;
218+ Map <String , String > expandedExtraConfigParameters ;
166219 EnvVars env ;
167220 try {
168221 env = run .getEnvironment (listener );
@@ -179,9 +232,29 @@ private boolean cloneFromSource(final Run<?, ?> run, final Launcher launcher, fi
179232 expandedFolder = env .expand (folder );
180233 expandedResourcePool = env .expand (resourcePool );
181234 expandedCustomizationSpec = env .expand (customizationSpec );
235+ if (namedSnapshot != null ) {
236+ expandedNamedSnapshot = env .expand (namedSnapshot );
237+ }
182238 }
183- vsphere .cloneVm (expandedClone , expandedSource , linkedClone , expandedResourcePool , expandedCluster ,
184- expandedDatastore , expandedFolder , powerOn , expandedCustomizationSpec , jLogger );
239+
240+ if (extraConfigParameters != null && !(extraConfigParameters .isEmpty ())) {
241+ // Always pass a copy of the non-trivial original parameter map
242+ // (expanded or not), just in case, to protect caller's data.
243+ expandedExtraConfigParameters = new HashMap <String , String >();
244+ if (run instanceof AbstractBuild ) {
245+ extraConfigParameters .forEach ((k , v ) -> expandedExtraConfigParameters .put (k , env .expand (v )));
246+ } else {
247+ expandedExtraConfigParameters .putAll (extraConfigParameters );
248+ }
249+ } else {
250+ // Only init to null here, due to lambda used in forEach() above
251+ expandedExtraConfigParameters = null ;
252+ }
253+
254+ vsphere .cloneOrDeployVm (expandedClone , expandedSource , linkedClone , expandedResourcePool , expandedCluster ,
255+ expandedDatastore , expandedFolder , this .isUseCurrentSnapshot (), expandedNamedSnapshot ,
256+ powerOn , expandedExtraConfigParameters , expandedCustomizationSpec , jLogger );
257+
185258 final int timeoutInSecondsForGetIp = getTimeoutInSeconds ();
186259 if (powerOn && timeoutInSecondsForGetIp >0 ) {
187260 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,
264337 @ QueryParameter String serverName ,
265338 @ QueryParameter String sourceName , @ QueryParameter String clone ,
266339 @ QueryParameter String resourcePool , @ QueryParameter String cluster ,
267- @ QueryParameter String customizationSpec ) {
340+ @ QueryParameter String customizationSpec ,
341+ @ QueryParameter Boolean linkedClone ,
342+ @ QueryParameter Boolean useCurrentSnapshot ,
343+ @ QueryParameter String namedSnapshot ) {
344+ // TODO? @QueryParameter Map<String, String> extraConfigParameters
268345 throwUnlessUserHasPermissionToConfigureJob (context );
269346 try {
270347 if (sourceName .length () == 0 || clone .length ()==0 || serverName .length ()==0
@@ -285,9 +362,21 @@ public FormValidation doTestData(@AncestorInPath Item context,
285362 if (vm == null )
286363 return FormValidation .error (Messages .validation_notFound ("sourceName" ));
287364
288- VirtualMachineSnapshot snap = vm .getCurrentSnapShot ();
289- if (snap == null )
290- return FormValidation .error (Messages .validation_noSnapshots ());
365+ if (linkedClone || useCurrentSnapshot || (namedSnapshot != null && !(namedSnapshot .isEmpty ()))) {
366+ // Use-case (according to parameters) requires a snapshot
367+ VirtualMachineSnapshot snap ;
368+ if (namedSnapshot == null || namedSnapshot .isEmpty ()) {
369+ // either useCurrentSnapshot or linkedClone is true
370+ snap = vm .getCurrentSnapShot ();
371+ } else {
372+ // namedSnapshot is non-trivial
373+ if (useCurrentSnapshot )
374+ return FormValidation .error (Messages .validation_useCurrentAndNamedSnapshots ());
375+ snap = vsphere .getSnapshotInTree (vm , namedSnapshot );
376+ }
377+ if (snap == null )
378+ return FormValidation .error (Messages .validation_noSnapshots ());
379+ }
291380
292381 if (customizationSpec != null && customizationSpec .length () > 0 &&
293382 vsphere .getCustomizationSpecByName (customizationSpec ) == null ) {
0 commit comments