OneEC2Slave.java 15 KB

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