OneSlaveTemplate.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. package hudson.plugins.ec2.one;
  2. import hudson.Extension;
  3. import hudson.Util;
  4. import hudson.model.Describable;
  5. import hudson.model.TaskListener;
  6. import hudson.model.Descriptor;
  7. import hudson.model.Descriptor.FormException;
  8. import hudson.model.Hudson;
  9. import hudson.model.Label;
  10. import hudson.model.Node;
  11. import hudson.model.labels.LabelAtom;
  12. import hudson.plugins.ec2.EC2Computer;
  13. import hudson.plugins.ec2.EC2Slave;
  14. import hudson.util.FormValidation;
  15. import java.io.IOException;
  16. import java.io.PrintStream;
  17. import java.net.URL;
  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.HashSet;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.Set;
  25. import javax.servlet.ServletException;
  26. import org.apache.commons.codec.binary.Base64;
  27. import org.apache.commons.lang.StringUtils;
  28. import org.kohsuke.stapler.DataBoundConstructor;
  29. import org.kohsuke.stapler.QueryParameter;
  30. import com.amazonaws.AmazonClientException;
  31. import com.amazonaws.services.ec2.AmazonEC2;
  32. import com.amazonaws.services.ec2.model.CreateTagsRequest;
  33. import com.amazonaws.services.ec2.model.DescribeImagesRequest;
  34. import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
  35. import com.amazonaws.services.ec2.model.DescribeInstancesResult;
  36. import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
  37. import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
  38. import com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
  39. import com.amazonaws.services.ec2.model.DescribeSubnetsResult;
  40. import com.amazonaws.services.ec2.model.Filter;
  41. import com.amazonaws.services.ec2.model.Image;
  42. import com.amazonaws.services.ec2.model.Instance;
  43. import com.amazonaws.services.ec2.model.InstanceStateName;
  44. import com.amazonaws.services.ec2.model.KeyPair;
  45. import com.amazonaws.services.ec2.model.Placement;
  46. import com.amazonaws.services.ec2.model.Reservation;
  47. import com.amazonaws.services.ec2.model.RunInstancesRequest;
  48. import com.amazonaws.services.ec2.model.SecurityGroup;
  49. import com.amazonaws.services.ec2.model.StartInstancesRequest;
  50. import com.amazonaws.services.ec2.model.StartInstancesResult;
  51. import com.amazonaws.services.ec2.model.Tag;
  52. /**
  53. * Template of {@link EC2Slave} to launch.
  54. *
  55. * @author Kohsuke Kawaguchi
  56. */
  57. public class OneSlaveTemplate implements Describable<OneSlaveTemplate> {
  58. public final String ami;
  59. public final String description;
  60. public final String zone;
  61. public final String securityGroups;
  62. public final String remoteFS;
  63. public final String sshPort;
  64. public final OneInstanceType type;
  65. public final String labels;
  66. public final String initScript;
  67. public final String userData;
  68. public final String numExecutors;
  69. public final String remoteAdmin;
  70. public final String rootCommandPrefix;
  71. public final String jvmopts;
  72. public final String subnetId;
  73. public final String idleTerminationMinutes;
  74. public final int instanceCap;
  75. public final boolean stopOnTerminate;
  76. private final List<OneEC2Tag> tags;
  77. public final boolean usePrivateDnsName;
  78. protected transient OneEC2Cloud parent;
  79. private transient/* almost final */Set<LabelAtom> labelSet;
  80. private transient/* almost final */Set<String> securityGroupSet;
  81. @DataBoundConstructor
  82. public OneSlaveTemplate(final String ami, final String zone,
  83. final String securityGroups, final String remoteFS,
  84. final String sshPort, final OneInstanceType type,
  85. final String labelString, final String description,
  86. final String initScript, final String userData,
  87. final String numExecutors, final String remoteAdmin,
  88. final String rootCommandPrefix, final String jvmopts,
  89. final boolean stopOnTerminate, final String subnetId,
  90. final List<OneEC2Tag> tags,
  91. final String idleTerminationMinutes,
  92. final boolean usePrivateDnsName, final String instanceCapStr) {
  93. this.ami = ami;
  94. this.zone = zone;
  95. this.securityGroups = securityGroups;
  96. this.remoteFS = remoteFS;
  97. this.sshPort = sshPort;
  98. this.type = type;
  99. labels = Util.fixNull(labelString);
  100. this.description = description;
  101. this.initScript = initScript;
  102. this.userData = userData;
  103. this.numExecutors = Util.fixNull(numExecutors).trim();
  104. this.remoteAdmin = remoteAdmin;
  105. this.rootCommandPrefix = rootCommandPrefix;
  106. this.jvmopts = jvmopts;
  107. this.stopOnTerminate = stopOnTerminate;
  108. this.subnetId = subnetId;
  109. this.tags = tags;
  110. this.idleTerminationMinutes = idleTerminationMinutes;
  111. this.usePrivateDnsName = usePrivateDnsName;
  112. if (null == instanceCapStr || instanceCapStr.equals("")) {
  113. instanceCap = Integer.MAX_VALUE;
  114. } else {
  115. instanceCap = Integer.parseInt(instanceCapStr);
  116. }
  117. readResolve(); // initialize
  118. }
  119. public OneEC2Cloud getParent() {
  120. return parent;
  121. }
  122. public String getLabelString() {
  123. return labels;
  124. }
  125. public String getDisplayName() {
  126. return description + " (" + ami + ")";
  127. }
  128. String getZone() {
  129. return zone;
  130. }
  131. public String getSecurityGroupString() {
  132. return securityGroups;
  133. }
  134. public Set<String> getSecurityGroupSet() {
  135. return securityGroupSet;
  136. }
  137. public Set<String> parseSecurityGroups() {
  138. if (securityGroups == null || "".equals(securityGroups.trim())) {
  139. return Collections.emptySet();
  140. } else {
  141. return new HashSet<String>(Arrays.asList(securityGroups
  142. .split("\\s*,\\s*")));
  143. }
  144. }
  145. public int getNumExecutors() {
  146. try {
  147. return Integer.parseInt(numExecutors);
  148. } catch (NumberFormatException e) {
  149. return OneEC2Slave.toNumExecutors(type);
  150. }
  151. }
  152. public int getSshPort() {
  153. try {
  154. return Integer.parseInt(sshPort);
  155. } catch (NumberFormatException e) {
  156. return 22;
  157. }
  158. }
  159. public String getRemoteAdmin() {
  160. return remoteAdmin;
  161. }
  162. public String getRootCommandPrefix() {
  163. return rootCommandPrefix;
  164. }
  165. public String getSubnetId() {
  166. return subnetId;
  167. }
  168. public List<OneEC2Tag> getTags() {
  169. if (null == tags) {
  170. return null;
  171. }
  172. return Collections.unmodifiableList(tags);
  173. }
  174. public String getidleTerminationMinutes() {
  175. return idleTerminationMinutes;
  176. }
  177. public Set<LabelAtom> getLabelSet() {
  178. return labelSet;
  179. }
  180. public int getInstanceCap() {
  181. return instanceCap;
  182. }
  183. public String getInstanceCapStr() {
  184. if (instanceCap == Integer.MAX_VALUE) {
  185. return "";
  186. } else {
  187. return String.valueOf(instanceCap);
  188. }
  189. }
  190. /**
  191. * Does this contain the given label?
  192. *
  193. * @param l
  194. * can be null to indicate "don't care".
  195. */
  196. public boolean containsLabel(final Label l) {
  197. return l == null || labelSet.contains(l);
  198. }
  199. /**
  200. * Provisions a new EC2 slave.
  201. *
  202. * @return always non-null. This needs to be then added to
  203. * {@link Hudson#addNode(Node)}.
  204. */
  205. public OneEC2Slave provision(final TaskListener listener)
  206. throws AmazonClientException, IOException {
  207. PrintStream logger = listener.getLogger();
  208. AmazonEC2 ec2 = getParent().connect();
  209. try {
  210. logger.println("Launching " + ami);
  211. KeyPair keyPair =
  212. (null != parent.getPrivateKey()) ? parent.getPrivateKey()
  213. .find(ec2) : null;
  214. RunInstancesRequest riRequest =
  215. new RunInstancesRequest(ami, 1, 1);
  216. List<Filter> diFilters = new ArrayList<Filter>();
  217. diFilters.add(new Filter("image-id").withValues(ami));
  218. if (StringUtils.isNotBlank(getZone())) {
  219. Placement placement = new Placement(getZone());
  220. riRequest.setPlacement(placement);
  221. diFilters.add(new Filter("availability-zone")
  222. .withValues(getZone()));
  223. }
  224. if (StringUtils.isNotBlank(getSubnetId())) {
  225. riRequest.setSubnetId(getSubnetId());
  226. diFilters.add(new Filter("subnet-id")
  227. .withValues(getSubnetId()));
  228. /*
  229. * If we have a subnet ID then we can only use VPC security
  230. * groups
  231. */
  232. if (!securityGroupSet.isEmpty()) {
  233. List<String> group_ids = new ArrayList<String>();
  234. DescribeSecurityGroupsRequest group_req =
  235. new DescribeSecurityGroupsRequest();
  236. group_req.withFilters(new Filter("group-name")
  237. .withValues(securityGroupSet));
  238. DescribeSecurityGroupsResult group_result =
  239. ec2.describeSecurityGroups(group_req);
  240. for (SecurityGroup group : group_result
  241. .getSecurityGroups()) {
  242. if (group.getVpcId() != null
  243. && !group.getVpcId().isEmpty()) {
  244. List<Filter> filters = new ArrayList<Filter>();
  245. filters.add(new Filter("vpc-id")
  246. .withValues(group.getVpcId()));
  247. filters.add(new Filter("state")
  248. .withValues("available"));
  249. filters.add(new Filter("subnet-id")
  250. .withValues(getSubnetId()));
  251. DescribeSubnetsRequest subnet_req =
  252. new DescribeSubnetsRequest();
  253. subnet_req.withFilters(filters);
  254. DescribeSubnetsResult subnet_result =
  255. ec2.describeSubnets(subnet_req);
  256. List subnets = subnet_result.getSubnets();
  257. if (subnets != null && !subnets.isEmpty()) {
  258. group_ids.add(group.getGroupId());
  259. }
  260. }
  261. }
  262. if (securityGroupSet.size() != group_ids.size()) {
  263. throw new AmazonClientException(
  264. "Security groups must all be VPC security groups to work in a VPC context");
  265. }
  266. if (!group_ids.isEmpty()) {
  267. riRequest.setSecurityGroupIds(group_ids);
  268. diFilters.add(new Filter("instance.group-id")
  269. .withValues(group_ids));
  270. }
  271. }
  272. } else {
  273. /* No subnet: we can use standard security groups by name */
  274. riRequest.setSecurityGroups(securityGroupSet);
  275. if (securityGroupSet.size() > 0) {
  276. diFilters.add(new Filter("group-name")
  277. .withValues(securityGroupSet));
  278. }
  279. }
  280. if (null != userData) {
  281. String userDataString =
  282. Base64.encodeBase64String(userData.getBytes());
  283. riRequest.setUserData(userDataString);
  284. }
  285. if (null != keyPair) {
  286. riRequest.setKeyName(keyPair.getKeyName());
  287. diFilters.add(new Filter("key-name").withValues(keyPair
  288. .getKeyName()));
  289. }
  290. riRequest.setInstanceType(type.toString());
  291. diFilters.add(new Filter("instance-type").withValues(type
  292. .toString()));
  293. HashSet<Tag> inst_tags = null;
  294. if (tags != null && !tags.isEmpty()) {
  295. inst_tags = new HashSet<Tag>();
  296. for (OneEC2Tag t : tags) {
  297. diFilters.add(new Filter("tag:" + t.getName())
  298. .withValues(t.getValue()));
  299. }
  300. }
  301. DescribeInstancesRequest diRequest =
  302. new DescribeInstancesRequest();
  303. diFilters.add(new Filter("instance-state-name").withValues(
  304. InstanceStateName.Stopped.toString(),
  305. InstanceStateName.Stopping.toString()));
  306. diRequest.setFilters(diFilters);
  307. logger.println("Looking for existing instances: " + diRequest);
  308. DescribeInstancesResult diResult =
  309. ec2.describeInstances(diRequest);
  310. filterReservations(diResult.getReservations(), diFilters);
  311. if (diResult.getReservations().size() == 0) {
  312. // Have to create a new instance
  313. Instance inst =
  314. ec2.runInstances(riRequest).getReservation()
  315. .getInstances().get(0);
  316. /* Now that we have our instance, we can set tags on it */
  317. if (inst_tags != null) {
  318. CreateTagsRequest tag_request =
  319. new CreateTagsRequest();
  320. tag_request.withResources(inst.getInstanceId())
  321. .setTags(inst_tags);
  322. ec2.createTags(tag_request);
  323. // That was a remote request - we should also update our
  324. // local instance data.
  325. inst.setTags(inst_tags);
  326. }
  327. logger.println("No existing instance found - created: "
  328. + inst);
  329. return newSlave(inst);
  330. }
  331. Instance inst =
  332. diResult.getReservations().get(0).getInstances().get(0);
  333. logger.println("Found existing stopped instance: " + inst);
  334. List<String> instances = new ArrayList<String>();
  335. instances.add(inst.getInstanceId());
  336. StartInstancesRequest siRequest =
  337. new StartInstancesRequest(instances);
  338. StartInstancesResult siResult = ec2.startInstances(siRequest);
  339. logger.println("Starting existing instance: " + inst
  340. + " result:" + siResult);
  341. List<Node> nodes = Hudson.getInstance().getNodes();
  342. for (int i = 0, len = nodes.size(); i < len; i++) {
  343. if (!(nodes.get(i) instanceof EC2Slave)) {
  344. continue;
  345. }
  346. OneEC2Slave ec2Node = (OneEC2Slave) nodes.get(i);
  347. if (ec2Node.getInstanceId().equals(inst.getInstanceId())) {
  348. logger.println("Found existing corresponding: "
  349. + ec2Node);
  350. return ec2Node;
  351. }
  352. }
  353. // Existing slave not found
  354. logger.println("Creating new slave for existing instance: "
  355. + inst);
  356. return newSlave(inst);
  357. } catch (FormException e) {
  358. throw new AssertionError(); // we should have discovered all
  359. // configuration issues upfront
  360. }
  361. }
  362. private OneEC2Slave newSlave(final Instance inst)
  363. throws FormException, IOException {
  364. return new OneEC2Slave(inst.getInstanceId(), description,
  365. remoteFS, getSshPort(), getNumExecutors(), labels, initScript,
  366. remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate,
  367. idleTerminationMinutes, inst.getPublicDnsName(),
  368. inst.getPrivateDnsName(), OneEC2Tag.fromAmazonTags(inst
  369. .getTags()), usePrivateDnsName);
  370. }
  371. /**
  372. * Provisions a new EC2 slave based on the currently running instance on
  373. * EC2, instead of starting a new one.
  374. */
  375. public OneEC2Slave attach(final String instanceId,
  376. final TaskListener listener)
  377. throws AmazonClientException, IOException {
  378. PrintStream logger = listener.getLogger();
  379. AmazonEC2 ec2 = getParent().connect();
  380. try {
  381. logger.println("Attaching to " + instanceId);
  382. DescribeInstancesRequest request =
  383. new DescribeInstancesRequest();
  384. request.setInstanceIds(Collections.singletonList(instanceId));
  385. Instance inst =
  386. ec2.describeInstances(request).getReservations().get(0)
  387. .getInstances().get(0);
  388. return newSlave(inst);
  389. } catch (FormException e) {
  390. throw new AssertionError(); // we should have discovered all
  391. // configuration issues upfront
  392. }
  393. }
  394. /**
  395. * Initializes data structure that we don't persist.
  396. */
  397. protected Object readResolve() {
  398. labelSet = Label.parse(labels);
  399. securityGroupSet = parseSecurityGroups();
  400. return this;
  401. }
  402. public Descriptor<OneSlaveTemplate> getDescriptor() {
  403. return Hudson.getInstance().getDescriptor(getClass());
  404. }
  405. @Extension
  406. public static final class DescriptorImpl extends
  407. Descriptor<OneSlaveTemplate> {
  408. @Override
  409. public String getDisplayName() {
  410. return null;
  411. }
  412. /**
  413. * Since this shares much of the configuration with {@link EC2Computer},
  414. * check its help page, too.
  415. */
  416. @Override
  417. public String getHelpFile(final String fieldName) {
  418. String p = super.getHelpFile(fieldName);
  419. if (p == null) {
  420. p =
  421. Hudson.getInstance().getDescriptor(OneEC2Slave.class)
  422. .getHelpFile(fieldName);
  423. }
  424. return p;
  425. }
  426. /***
  427. * Check that the AMI requested is available in the cloud and can be
  428. * used.
  429. */
  430. public FormValidation doValidateAmi(
  431. @QueryParameter final String url,
  432. @QueryParameter final String accessId,
  433. @QueryParameter final String secretKey,
  434. final @QueryParameter String ami)
  435. throws IOException, ServletException {
  436. AmazonEC2 ec2 =
  437. OneEC2Cloud.connect(accessId, secretKey, new URL(url));
  438. if (ec2 != null) {
  439. try {
  440. List<String> images = new LinkedList<String>();
  441. images.add(ami);
  442. List<String> owners = new LinkedList<String>();
  443. List<String> users = new LinkedList<String>();
  444. DescribeImagesRequest request =
  445. new DescribeImagesRequest();
  446. request.setImageIds(images);
  447. request.setOwners(owners);
  448. request.setExecutableUsers(users);
  449. List<Image> img =
  450. ec2.describeImages(request).getImages();
  451. if (img == null || img.isEmpty()) {
  452. // de-registered AMI causes an empty list to be
  453. // returned. so be defensive
  454. // against other possibilities
  455. return FormValidation
  456. .error("No such AMI, or not usable with this accessId: "
  457. + ami);
  458. }
  459. return FormValidation.ok(img.get(0).getImageLocation()
  460. + " by " + img.get(0).getImageOwnerAlias());
  461. } catch (AmazonClientException e) {
  462. return FormValidation.error(e.getMessage());
  463. }
  464. } else {
  465. return FormValidation.ok(); // can't test
  466. }
  467. }
  468. public FormValidation doCheckIdleTerminationMinutes(
  469. @QueryParameter final String value) {
  470. if (value == null || value.trim() == "") {
  471. return FormValidation.ok();
  472. }
  473. try {
  474. int val = Integer.parseInt(value);
  475. if (val >= 0) {
  476. return FormValidation.ok();
  477. }
  478. } catch (NumberFormatException nfe) {
  479. }
  480. return FormValidation
  481. .error("Idle Termination time must be a non-negative integer (or null)");
  482. }
  483. public FormValidation doCheckInstanceCapStr(
  484. @QueryParameter final String value) {
  485. if (value == null || value.trim() == "") {
  486. return FormValidation.ok();
  487. }
  488. try {
  489. int val = Integer.parseInt(value);
  490. if (val >= 0) {
  491. return FormValidation.ok();
  492. }
  493. } catch (NumberFormatException nfe) {
  494. }
  495. return FormValidation
  496. .error("InstanceCap must be a non-negative integer (or null)");
  497. }
  498. }
  499. private void filterReservations(final List<Reservation> reservations,
  500. final List<Filter> filters) {
  501. for (Filter f : filters) {
  502. filterReservationsBy(reservations, f.getName(), f.getValues());
  503. }
  504. }
  505. private void filterReservationsBy(
  506. final List<Reservation> reservations, final String name,
  507. final List<String> values) {
  508. List<Reservation> emptyReservations = new ArrayList<Reservation>();
  509. for (Reservation reservation : reservations) {
  510. filterReservationBy(reservation, name, values);
  511. if (reservation.getInstances().size() == 0) {
  512. emptyReservations.add(reservation);
  513. }
  514. }
  515. reservations.removeAll(emptyReservations);
  516. }
  517. /**
  518. * @param reservation
  519. * @param name
  520. * @param values
  521. */
  522. private void filterReservationBy(final Reservation reservation,
  523. final String name, final List<String> values) {
  524. List<Instance> instances = new ArrayList<Instance>();
  525. for (Instance inst : reservation.getInstances()) {
  526. if (name.equalsIgnoreCase("image-id")) {
  527. String data = inst.getImageId();
  528. for (String value : values) {
  529. if (value.equalsIgnoreCase(data)) {
  530. instances.add(inst);
  531. break;
  532. }
  533. }
  534. }
  535. if (name.equalsIgnoreCase("instance-type")) {
  536. String data = inst.getInstanceType();
  537. for (String value : values) {
  538. if (value.equalsIgnoreCase(data)) {
  539. instances.add(inst);
  540. break;
  541. }
  542. }
  543. }
  544. if (name.equalsIgnoreCase("instance-state-name")) {
  545. String data = inst.getState().getName();
  546. for (String value : values) {
  547. if (value.equalsIgnoreCase(data)) {
  548. instances.add(inst);
  549. break;
  550. }
  551. }
  552. }
  553. if (name.equalsIgnoreCase("key-name")) {
  554. String data = inst.getKeyName();
  555. for (String value : values) {
  556. if (value.equalsIgnoreCase(data)) {
  557. instances.add(inst);
  558. break;
  559. }
  560. }
  561. }
  562. // TODO: Include more filters
  563. // TODO: Improve filtering mecanism
  564. }
  565. reservation.getInstances().retainAll(instances);
  566. }
  567. }