EC2Slave.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package hudson.plugins.ec2;
  2. import hudson.Extension;
  3. import hudson.Util;
  4. import hudson.model.Computer;
  5. import hudson.model.Descriptor.FormException;
  6. import hudson.model.Hudson;
  7. import hudson.model.Slave;
  8. import hudson.model.Node;
  9. import hudson.plugins.ec2.ssh.EC2UnixLauncher;
  10. import hudson.slaves.NodeProperty;
  11. import hudson.util.ListBoxModel;
  12. import java.io.IOException;
  13. import java.util.Collections;
  14. import java.util.List;
  15. import java.util.LinkedList;
  16. import java.util.HashSet;
  17. import java.util.logging.Level;
  18. import java.util.logging.Logger;
  19. import javax.servlet.ServletException;
  20. import org.apache.commons.lang.StringUtils;
  21. import org.kohsuke.stapler.DataBoundConstructor;
  22. import org.kohsuke.stapler.QueryParameter;
  23. import org.kohsuke.stapler.StaplerRequest;
  24. import com.amazonaws.AmazonClientException;
  25. import com.amazonaws.services.ec2.AmazonEC2;
  26. import com.amazonaws.services.ec2.model.*;
  27. import net.sf.json.JSONObject;
  28. /**
  29. * Slave running on EC2.
  30. *
  31. * @author Kohsuke Kawaguchi
  32. */
  33. public final class EC2Slave extends Slave {
  34. /**
  35. * Comes from {@link SlaveTemplate#initScript}.
  36. */
  37. public final String initScript;
  38. public final String remoteAdmin; // e.g. 'ubuntu'
  39. public final String rootCommandPrefix; // e.g. 'sudo'
  40. public final String jvmopts; //e.g. -Xmx1g
  41. public final boolean stopOnTerminate;
  42. public final String idleTerminationMinutes;
  43. public final boolean usePrivateDnsName;
  44. public List<EC2Tag> tags;
  45. // Temporary stuff that is obtained live from EC2
  46. public String publicDNS;
  47. public String privateDNS;
  48. /* The last instance data to be fetched for the slave */
  49. private Instance lastFetchInstance = null;
  50. /* The time at which we fetched the last instance data */
  51. private long lastFetchTime = 0;
  52. /* The time (in milliseconds) after which we will always re-fetch externally changeable EC2 data when we are asked for it */
  53. private static final long MIN_FETCH_TIME = 20 * 1000;
  54. /**
  55. * For data read from old Hudson, this is 0, so we use that to indicate 22.
  56. */
  57. private final int sshPort;
  58. public static final String TEST_ZONE = "testZone";
  59. public EC2Slave(String instanceId, String description, String remoteFS, int sshPort, int numExecutors, String labelString, String initScript, String remoteAdmin, String rootCommandPrefix, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List<EC2Tag> tags) throws FormException, IOException {
  60. this(instanceId, description, remoteFS, sshPort, numExecutors, Mode.NORMAL, labelString, initScript, Collections.<NodeProperty<?>>emptyList(), remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, false);
  61. }
  62. public EC2Slave(String instanceId, String description, String remoteFS, int sshPort, int numExecutors, String labelString, String initScript, String remoteAdmin, String rootCommandPrefix, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List<EC2Tag> tags, boolean usePrivateDnsName) throws FormException, IOException {
  63. this(instanceId, description, remoteFS, sshPort, numExecutors, Mode.NORMAL, labelString, initScript, Collections.<NodeProperty<?>>emptyList(), remoteAdmin, rootCommandPrefix, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, usePrivateDnsName);
  64. }
  65. @DataBoundConstructor
  66. public EC2Slave(String instanceId, String description, String remoteFS, int sshPort, int numExecutors, Mode mode, String labelString, String initScript, List<? extends NodeProperty<?>> nodeProperties, String remoteAdmin, String rootCommandPrefix, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List<EC2Tag> tags, boolean usePrivateDnsName) throws FormException, IOException {
  67. super(instanceId, description, remoteFS, numExecutors, mode, labelString, new EC2UnixLauncher(), new EC2RetentionStrategy(idleTerminationMinutes), nodeProperties);
  68. this.initScript = initScript;
  69. this.remoteAdmin = remoteAdmin;
  70. this.rootCommandPrefix = rootCommandPrefix;
  71. this.jvmopts = jvmopts;
  72. this.sshPort = sshPort;
  73. this.stopOnTerminate = stopOnTerminate;
  74. this.idleTerminationMinutes = idleTerminationMinutes;
  75. this.publicDNS = publicDNS;
  76. this.privateDNS = privateDNS;
  77. this.tags = tags;
  78. this.usePrivateDnsName = usePrivateDnsName;
  79. }
  80. /**
  81. * Constructor for debugging.
  82. */
  83. public EC2Slave(String instanceId) throws FormException, IOException {
  84. this(instanceId,"debug", "/tmp/hudson", 22, 1, Mode.NORMAL, "debug", "", Collections.<NodeProperty<?>>emptyList(), null, null, null, false, null, "Fake public", "Fake private", null, false);
  85. }
  86. /**
  87. * See http://aws.amazon.com/ec2/instance-types/
  88. */
  89. /*package*/ static int toNumExecutors(InstanceType it) {
  90. switch (it) {
  91. case T1Micro: return 1;
  92. case M1Small: return 1;
  93. case M1Medium: return 2;
  94. case M1Large: return 4;
  95. case C1Medium: return 5;
  96. case M2Xlarge: return 6;
  97. case M1Xlarge: return 8;
  98. case M22xlarge: return 13;
  99. case C1Xlarge: return 20;
  100. case M24xlarge: return 26;
  101. case Cc14xlarge: return 33;
  102. case Cg14xlarge: return 33;
  103. default: throw new AssertionError();
  104. }
  105. }
  106. /**
  107. * EC2 instance ID.
  108. */
  109. public String getInstanceId() {
  110. return getNodeName();
  111. }
  112. @Override
  113. public Computer createComputer() {
  114. return new EC2Computer(this);
  115. }
  116. public static Instance getInstance(String instanceId) {
  117. DescribeInstancesRequest request = new DescribeInstancesRequest();
  118. request.setInstanceIds(Collections.<String>singletonList(instanceId));
  119. EC2Cloud cloudInstance = EC2Cloud.get();
  120. if (cloudInstance == null)
  121. return null;
  122. AmazonEC2 ec2 = cloudInstance.connect();
  123. List<Reservation> reservations = ec2.describeInstances(request).getReservations();
  124. Instance i = null;
  125. if (reservations.size() > 0) {
  126. List<Instance> instances = reservations.get(0).getInstances();
  127. if (instances.size() > 0)
  128. i = instances.get(0);
  129. }
  130. return i;
  131. }
  132. /**
  133. * Terminates the instance in EC2.
  134. */
  135. public void terminate() {
  136. try {
  137. if (!isAlive(true)) {
  138. /* The node has been killed externally, so we've nothing to do here */
  139. LOGGER.info("EC2 instance already terminated: "+getInstanceId());
  140. } else {
  141. AmazonEC2 ec2 = EC2Cloud.get().connect();
  142. TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId()));
  143. ec2.terminateInstances(request);
  144. LOGGER.info("Terminated EC2 instance (terminated): "+getInstanceId());
  145. }
  146. Hudson.getInstance().removeNode(this);
  147. } catch (AmazonClientException e) {
  148. LOGGER.log(Level.WARNING,"Failed to terminate EC2 instance: "+getInstanceId(),e);
  149. } catch (IOException e) {
  150. LOGGER.log(Level.WARNING,"Failed to terminate EC2 instance: "+getInstanceId(),e);
  151. }
  152. }
  153. void idleTimeout() {
  154. LOGGER.info("EC2 instance idle time expired: "+getInstanceId());
  155. if (!stopOnTerminate) {
  156. terminate();
  157. return;
  158. }
  159. try {
  160. AmazonEC2 ec2 = EC2Cloud.get().connect();
  161. StopInstancesRequest request = new StopInstancesRequest(
  162. Collections.singletonList(getInstanceId()));
  163. ec2.stopInstances(request);
  164. toComputer().disconnect(null);
  165. } catch (AmazonClientException e) {
  166. Instance i = getInstance(getNodeName());
  167. LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: "+getInstanceId() + " info: "+((i != null)?i:"") , e);
  168. }
  169. LOGGER.info("EC2 instance stopped: " + getInstanceId());
  170. }
  171. String getRemoteAdmin() {
  172. if (remoteAdmin == null || remoteAdmin.length() == 0)
  173. return "root";
  174. return remoteAdmin;
  175. }
  176. String getRootCommandPrefix() {
  177. if (rootCommandPrefix == null || rootCommandPrefix.length() == 0)
  178. return "";
  179. return rootCommandPrefix + " ";
  180. }
  181. String getJvmopts() {
  182. return Util.fixNull(jvmopts);
  183. }
  184. public int getSshPort() {
  185. return sshPort!=0 ? sshPort : 22;
  186. }
  187. public boolean getStopOnTerminate() {
  188. return stopOnTerminate;
  189. }
  190. private boolean isAlive(boolean force) {
  191. fetchLiveInstanceData(force);
  192. if (lastFetchInstance == null) return false;
  193. if (lastFetchInstance.getState().getName().equals(InstanceStateName.Terminated.toString())) return false;
  194. return true;
  195. }
  196. /* Much of the EC2 data is beyond our direct control, therefore we need to refresh it from time to
  197. time to ensure we reflect the reality of the instances. */
  198. private void fetchLiveInstanceData( boolean force ) throws AmazonClientException {
  199. /* If we've grabbed the data recently, don't bother getting it again unless we are forced */
  200. long now = System.currentTimeMillis();
  201. if ((lastFetchTime > 0) && (now - lastFetchTime < MIN_FETCH_TIME) && !force) {
  202. return;
  203. }
  204. Instance i = getInstance(getNodeName());
  205. lastFetchTime = now;
  206. lastFetchInstance = i;
  207. if (i == null)
  208. return;
  209. publicDNS = i.getPublicDnsName();
  210. privateDNS = i.getPrivateIpAddress();
  211. tags = new LinkedList<EC2Tag>();
  212. for (Tag t : i.getTags()) {
  213. tags.add(new EC2Tag(t.getKey(), t.getValue()));
  214. }
  215. }
  216. /* Clears all existing tag data so that we can force the instance into a known state */
  217. private void clearLiveInstancedata() throws AmazonClientException {
  218. Instance inst = getInstance(getNodeName());
  219. /* Now that we have our instance, we can clear the tags on it */
  220. if (!tags.isEmpty()) {
  221. HashSet<Tag> inst_tags = new HashSet<Tag>();
  222. for(EC2Tag t : tags) {
  223. inst_tags.add(new Tag(t.getName(), t.getValue()));
  224. }
  225. DeleteTagsRequest tag_request = new DeleteTagsRequest();
  226. tag_request.withResources(inst.getInstanceId()).setTags(inst_tags);
  227. EC2Cloud.get().connect().deleteTags(tag_request);
  228. }
  229. }
  230. /* Sets tags on an instance. This will not clear existing tag data, so call clearLiveInstancedata if needed */
  231. private void pushLiveInstancedata() throws AmazonClientException {
  232. Instance inst = getInstance(getNodeName());
  233. /* Now that we have our instance, we can set tags on it */
  234. if (tags != null && !tags.isEmpty()) {
  235. HashSet<Tag> inst_tags = new HashSet<Tag>();
  236. for(EC2Tag t : tags) {
  237. inst_tags.add(new Tag(t.getName(), t.getValue()));
  238. }
  239. CreateTagsRequest tag_request = new CreateTagsRequest();
  240. tag_request.withResources(inst.getInstanceId()).setTags(inst_tags);
  241. EC2Cloud.get().connect().createTags(tag_request);
  242. }
  243. }
  244. public String getPublicDNS() {
  245. fetchLiveInstanceData(false);
  246. return publicDNS;
  247. }
  248. public String getPrivateDNS() {
  249. fetchLiveInstanceData(false);
  250. return privateDNS;
  251. }
  252. public List<EC2Tag> getTags() {
  253. fetchLiveInstanceData(false);
  254. return Collections.unmodifiableList(tags);
  255. }
  256. @Override
  257. public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException {
  258. if (form == null) {
  259. return null;
  260. }
  261. if (!isAlive(true)) {
  262. LOGGER.info("EC2 instance terminated externally: " + getInstanceId());
  263. try {
  264. Hudson.getInstance().removeNode(this);
  265. } catch (IOException ioe) {
  266. LOGGER.log(Level.WARNING, "Attempt to reconfigure EC2 instance which has been externally terminated: " + getInstanceId(), ioe);
  267. }
  268. return null;
  269. }
  270. Node result = super.reconfigure(req, form);
  271. /* Get rid of the old tags, as represented by ourselves. */
  272. clearLiveInstancedata();
  273. /* Set the new tags, as represented by our successor */
  274. ((EC2Slave) result).pushLiveInstancedata();
  275. return result;
  276. }
  277. public boolean getUsePrivateDnsName() {
  278. return usePrivateDnsName;
  279. }
  280. public static ListBoxModel fillZoneItems(String accessId, String secretKey, String region) throws IOException, ServletException {
  281. ListBoxModel model = new ListBoxModel();
  282. if (AmazonEC2Cloud.testMode) {
  283. model.add(TEST_ZONE);
  284. return model;
  285. }
  286. if (!StringUtils.isEmpty(accessId) && !StringUtils.isEmpty(secretKey) && !StringUtils.isEmpty(region)) {
  287. AmazonEC2 client = EC2Cloud.connect(accessId, secretKey, AmazonEC2Cloud.getEc2EndpointUrl(region));
  288. DescribeAvailabilityZonesResult zones = client.describeAvailabilityZones();
  289. List<AvailabilityZone> zoneList = zones.getAvailabilityZones();
  290. model.add("<not specified>", "");
  291. for (AvailabilityZone z : zoneList) {
  292. model.add(z.getZoneName(), z.getZoneName());
  293. }
  294. }
  295. return model;
  296. }
  297. @Extension
  298. public static final class DescriptorImpl extends SlaveDescriptor {
  299. @Override
  300. public String getDisplayName() {
  301. return "Amazon EC2";
  302. }
  303. @Override
  304. public boolean isInstantiable() {
  305. return false;
  306. }
  307. public ListBoxModel doFillZoneItems(@QueryParameter String accessId,
  308. @QueryParameter String secretKey, @QueryParameter String region) throws IOException,
  309. ServletException {
  310. return fillZoneItems(accessId, secretKey, region);
  311. }
  312. }
  313. private static final Logger LOGGER = Logger.getLogger(EC2Slave.class.getName());
  314. }