26
26
import com .amazonaws .SdkClientException ;
27
27
import com .amazonaws .auth .AWSCredentialsProvider ;
28
28
import com .amazonaws .services .ec2 .AmazonEC2 ;
29
+ import com .amazonaws .services .ec2 .model .DescribeInstancesRequest ;
30
+ import com .amazonaws .services .ec2 .model .DescribeInstancesResult ;
29
31
import com .amazonaws .services .ec2 .model .DescribeRegionsResult ;
32
+ import com .amazonaws .services .ec2 .model .Filter ;
33
+ import com .amazonaws .services .ec2 .model .Instance ;
34
+ import com .amazonaws .services .ec2 .model .InstanceStateName ;
30
35
import com .amazonaws .services .ec2 .model .Region ;
36
+ import com .amazonaws .services .ec2 .model .Reservation ;
37
+ import com .amazonaws .services .ec2 .model .StartInstancesRequest ;
38
+ import com .amazonaws .services .ec2 .model .StopInstancesRequest ;
31
39
import com .google .common .annotations .VisibleForTesting ;
32
40
import hudson .Extension ;
33
41
import hudson .Util ;
42
+ import hudson .model .Computer ;
34
43
import hudson .model .Failure ;
44
+ import hudson .model .Node ;
45
+ import hudson .model .labels .LabelAtom ;
35
46
import hudson .plugins .ec2 .util .AmazonEC2Factory ;
36
47
import hudson .slaves .Cloud ;
48
+ import hudson .slaves .NodeProvisioner .PlannedNode ;
37
49
import hudson .util .FormValidation ;
38
50
import hudson .util .ListBoxModel ;
39
51
import java .io .IOException ;
40
52
import java .net .MalformedURLException ;
41
53
import java .net .URL ;
54
+ import java .util .Collections ;
42
55
import java .util .List ;
43
56
import java .util .Locale ;
57
+ import java .util .concurrent .TimeUnit ;
58
+ import java .util .logging .Level ;
59
+ import java .util .logging .Logger ;
44
60
import javax .annotation .Nullable ;
45
61
import javax .servlet .ServletException ;
46
62
import jenkins .model .Jenkins ;
47
63
import org .kohsuke .stapler .DataBoundConstructor ;
48
64
import org .kohsuke .stapler .DataBoundSetter ;
49
65
import org .kohsuke .stapler .QueryParameter ;
66
+ import org .kohsuke .stapler .StaplerResponse ;
50
67
import org .kohsuke .stapler .interceptor .RequirePOST ;
51
68
52
69
/**
@@ -62,10 +79,26 @@ public class AmazonEC2Cloud extends EC2Cloud {
62
79
63
80
private String altEC2Endpoint ;
64
81
82
+ private static final Logger LOGGER = Logger .getLogger (AmazonEC2Cloud .class .getName ());
83
+
65
84
public static final String CLOUD_ID_PREFIX = "ec2-" ;
66
85
86
+ private static final int MAX_RESULTS = 1000 ;
87
+
88
+ private static final String INSTANCE_NAME_TAG = "Name" ;
89
+
90
+ private static final String TAG_PREFIX = "tag" ;
91
+
67
92
private boolean noDelayProvisioning ;
68
93
94
+ private boolean startStopNodes ;
95
+
96
+ private String instanceTagForJenkins ;
97
+
98
+ private String nodeTagForEc2 ;
99
+
100
+ private String maxIdleMinutes ;
101
+
69
102
@ DataBoundConstructor
70
103
public AmazonEC2Cloud (String cloudName , boolean useInstanceProfileForCredentials , String credentialsId , String region , String privateKey , String instanceCapStr , List <? extends SlaveTemplate > templates , String roleArn , String roleSessionName ) {
71
104
super (createCloudId (cloudName ), useInstanceProfileForCredentials , credentialsId , privateKey , instanceCapStr , templates , roleArn , roleSessionName );
@@ -126,6 +159,24 @@ public void setNoDelayProvisioning(boolean noDelayProvisioning) {
126
159
this .noDelayProvisioning = noDelayProvisioning ;
127
160
}
128
161
162
+ @ DataBoundSetter
163
+ public void setStartStopNodes (boolean startStopNodes ) {
164
+ this .startStopNodes = startStopNodes ;
165
+ }
166
+
167
+ public boolean isStartStopNodes () {
168
+ return startStopNodes ;
169
+ }
170
+
171
+ public String getInstanceTagForJenkins () {
172
+ return instanceTagForJenkins ;
173
+ }
174
+
175
+ @ DataBoundSetter
176
+ public void setInstanceTagForJenkins (String instanceTagForJenkins ) {
177
+ this .instanceTagForJenkins = instanceTagForJenkins ;
178
+ }
179
+
129
180
public String getAltEC2Endpoint () {
130
181
return altEC2Endpoint ;
131
182
}
@@ -135,11 +186,159 @@ public void setAltEC2Endpoint(String altEC2Endpoint) {
135
186
this .altEC2Endpoint = altEC2Endpoint ;
136
187
}
137
188
189
+ public String getNodeTagForEc2 () {
190
+ return nodeTagForEc2 ;
191
+ }
192
+
193
+ @ DataBoundSetter
194
+ public void setNodeTagForEc2 (String nodeTagForEc2 ) {
195
+ this .nodeTagForEc2 = nodeTagForEc2 ;
196
+ }
197
+
198
+ public boolean isEc2Node (Node node ) {
199
+ //If no label is specified then we check all nodes
200
+ if (nodeTagForEc2 == null || nodeTagForEc2 .trim ().length () == 0 ) {
201
+ return true ;
202
+ }
203
+
204
+ for (LabelAtom label : node .getAssignedLabels ()) {
205
+ if (label .getExpression ().equalsIgnoreCase (nodeTagForEc2 )) {
206
+ return true ;
207
+ }
208
+ }
209
+ return false ;
210
+ }
211
+
212
+ public String getMaxIdleMinutes () {
213
+ return maxIdleMinutes ;
214
+ }
215
+
216
+ @ DataBoundSetter
217
+ public void setMaxIdleMinutes (String maxIdleMinutes ) {
218
+ this .maxIdleMinutes = maxIdleMinutes ;
219
+ }
220
+
221
+ public PlannedNode startNode (Node node ) {
222
+ Instance nodeInstance = getInstanceByLabel (node .getSelfLabel ().getExpression (), InstanceStateName .Stopped );
223
+ if (nodeInstance == null ) {
224
+ nodeInstance = getInstanceByNodeName (node .getNodeName (), InstanceStateName .Stopped );
225
+ }
226
+
227
+ if (nodeInstance == null ) {
228
+ return null ;
229
+ }
230
+
231
+ final String instanceId = nodeInstance .getInstanceId ();
232
+
233
+ return new PlannedNode (node .getDisplayName (),
234
+ Computer .threadPoolForRemoting .submit (() -> {
235
+ try {
236
+ while (true ) {
237
+ StartInstancesRequest startRequest = new StartInstancesRequest ();
238
+ startRequest .setInstanceIds (Collections .singletonList (instanceId ));
239
+ connect ().startInstances (startRequest );
240
+
241
+ Instance instance = CloudHelper .getInstanceWithRetry (instanceId , this );
242
+ if (instance == null ) {
243
+ LOGGER .log (Level .WARNING , "Can't find instance with instance id `{0}` in cloud {1}. Terminate provisioning " , new Object [] {
244
+ instanceId , this .getCloudName () });
245
+ return null ;
246
+ }
247
+
248
+ InstanceStateName state = InstanceStateName .fromValue (instance .getState ().getName ());
249
+ if (state .equals (InstanceStateName .Running )) {
250
+ long startTime = TimeUnit .MILLISECONDS .toSeconds (System .currentTimeMillis () - instance .getLaunchTime ().getTime ());
251
+ LOGGER .log (Level .INFO , "{0} moved to RUNNING state in {1} seconds and is ready to be connected by Jenkins" , new Object [] {
252
+ instanceId , startTime });
253
+ return node ;
254
+ }
255
+
256
+ if (!state .equals (InstanceStateName .Pending )) {
257
+ LOGGER .log (Level .WARNING , "{0}. Node {1} is neither pending nor running, it's {2}. Terminate provisioning" , new Object [] {
258
+ instanceId , node .getNodeName (), state });
259
+ return null ;
260
+ }
261
+
262
+ Thread .sleep (5000 );
263
+ }
264
+ } catch (Exception e ) {
265
+ LOGGER .log (Level .WARNING , "Unable to start " + instanceId , e );
266
+ return null ;
267
+ }
268
+ })
269
+ , node .getNumExecutors ());
270
+ }
271
+
272
+ public void stopNode (Node node ) {
273
+ Instance nodeInstance = getInstanceByLabel (node .getSelfLabel ().getExpression (), InstanceStateName .Running );
274
+ if (nodeInstance == null ) {
275
+ nodeInstance = getInstanceByNodeName (node .getNodeName (), InstanceStateName .Running );
276
+ }
277
+
278
+ if (nodeInstance == null ) {
279
+ return ;
280
+ }
281
+
282
+ final String instanceId = nodeInstance .getInstanceId ();
283
+
284
+ try {
285
+ StopInstancesRequest request = new StopInstancesRequest ();
286
+ request .setInstanceIds (Collections .singletonList (instanceId ));
287
+ connect ().stopInstances (request );
288
+ LOGGER .log (Level .INFO , "Stopped instance: {0}" , instanceId );
289
+ } catch (Exception e ) {
290
+ LOGGER .log (Level .INFO , "Unable to stop instance: " + instanceId , e );
291
+ }
292
+ }
293
+
138
294
@ Override
139
295
protected AWSCredentialsProvider createCredentialsProvider () {
140
296
return createCredentialsProvider (isUseInstanceProfileForCredentials (), getCredentialsId (), getRoleArn (), getRoleSessionName (), getRegion ());
141
297
}
142
298
299
+ private Instance getInstanceByLabel (String label , InstanceStateName desiredState ) {
300
+ String tag = getInstanceTagForJenkins ();
301
+ if (tag == null ) {
302
+ return null ;
303
+ }
304
+ return getInstance (Collections .singletonList (getTagFilter (tag , label )), desiredState );
305
+ }
306
+
307
+ private Instance getInstanceByNodeName (String name , InstanceStateName desiredState ) {
308
+ return getInstance (Collections .singletonList (getTagFilter (INSTANCE_NAME_TAG , name )), desiredState );
309
+ }
310
+
311
+ private Filter getTagFilter (String name , String value ) {
312
+ Filter filter = new Filter ();
313
+ filter .setName (TAG_PREFIX + ":" + name .trim ());
314
+ filter .setValues (Collections .singletonList (value .trim ()));
315
+ LOGGER .log (Level .FINEST ,"Created filter to query for instance: {0}" , filter );
316
+ return filter ;
317
+ }
318
+
319
+ private Instance getInstance (List <Filter > filters , InstanceStateName desiredState ) {
320
+ DescribeInstancesRequest request = new DescribeInstancesRequest ();
321
+ request .setFilters (filters );
322
+ request .setMaxResults (MAX_RESULTS );
323
+ request .setNextToken (null );
324
+ DescribeInstancesResult response = connect ().describeInstances ( request );
325
+
326
+ if (!response .getReservations ().isEmpty ()) {
327
+ for (Reservation reservation : response .getReservations ()) {
328
+ for (Instance instance : reservation .getInstances ()) {
329
+ com .amazonaws .services .ec2 .model .InstanceState state = instance .getState ();
330
+ LOGGER .log (Level .FINEST ,"Instance {0} state: {1}" , new Object [] {instance .getInstanceId (), state .getName ()});
331
+ if (state .getName ().equals (desiredState .toString ())) {
332
+ return instance ;
333
+ }
334
+ }
335
+ }
336
+ } else {
337
+ LOGGER .log (Level .FINEST ,"No instances found that matched filter criteria" );
338
+ }
339
+ return null ;
340
+ }
341
+
143
342
@ Extension
144
343
public static class DescriptorImpl extends EC2Cloud .DescriptorImpl {
145
344
0 commit comments