azure slave plugin for jenkins

This commit is contained in:
snallami 2014-07-17 15:04:13 -07:00
parent 6508070d0a
commit a94971f0f8
55 changed files with 5400 additions and 3 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/bin
/target

View File

@ -1,5 +1,17 @@
azure-slaves-plugin
=============
azure-slave-plugin
==================
Jenkins-CI Plugin to create Azure slaves.
Jenkins Plugin to create Azure slaves
Supports creating
a) Windows slave on Azure Cloud using SSH and JNLP
- For windows images to launch via SSH, the image needs to be preconfigured with ssh.
For preparing custom windows image, refer to instructions @ http://azure.microsoft.com/en-us/documentation/articles/virtual-machines-capture-image-windows-server/
b) Linux slaves on Azure Cloud using SSH
For preparing custom linux image, refer to instructions @ http://azure.microsoft.com/en-us/documentation/articles/virtual-machines-linux-capture-image/

154
pom.xml Normal file
View File

@ -0,0 +1,154 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>1.509</version>
</parent>
<artifactId>azure-slave-plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>hpi</packaging>
<name>Azure Slave Plugin</name>
<description>Provisions slave on Azure cloud</description>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Azure+Slave+Plugin</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<licenses>
<license>
<name>Apache License, Version 2.0 (the "License")</name>
<comments>Licensed under the Apache License, Version 2.0 (the "License").</comments>
</license>
</licenses>
<developers>
<developer>
<id>martinsawicki</id>
<name>Martin Sawicki</name>
<email>marcins@microsoft.com</email>
</developer>
<developer>
<id>snallami</id>
<name>Suresh Nallamilli</name>
<email>snallami@gmail.com</email>
</developer>
</developers>
<dependencies>
<dependency>
<groupId>com.microsoft.windowsazure.storage</groupId>
<artifactId>microsoft-windowsazure-storage-sdk</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.windowsazure</groupId>
<artifactId>microsoft-azure-api-core</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.windowsazure</groupId>
<artifactId>microsoft-azure-api-management</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.windowsazure</groupId>
<artifactId>microsoft-azure-api-management-compute</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.windowsazure</groupId>
<artifactId>microsoft-azure-api-management-storage</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.3</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.18</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.18</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.51</version>
</dependency>
</dependencies>
<scm>
<connection>scm:git:ssh://git@github.com/jenkinsci/azure-slave-plugin.git</connection>
<developerConnection>scm:git:ssh://git@github.com/jenkinsci/azure-slave-plugin.git</developerConnection>
<url>https://github.com/jenkinsci/azure-slave-plugin</url>
</scm>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>maven-hpi-plugin</artifactId>
<version>1.95</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>2.5</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,379 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Label;
import hudson.model.Node;
import hudson.slaves.Cloud;
import hudson.slaves.NodeProvisioner.PlannedNode;
import hudson.slaves.OfflineCause;
import hudson.util.FormValidation;
import hudson.util.StreamTaskListener;
import com.microsoftopentechnologies.azure.AzureSlaveTemplate;
import com.microsoftopentechnologies.azure.util.AzureUtil;
import com.microsoftopentechnologies.azure.util.Constants;
public class AzureCloud extends Cloud {
private final String subscriptionId;
private final String serviceManagementCert;
private final String passPhrase="";
private final String serviceManagementURL;
private final int maxVirtualMachinesLimit;
private final List<AzureSlaveTemplate> instTemplates;
public static final Logger LOGGER = Logger.getLogger(AzureCloud.class.getName());
@DataBoundConstructor
public AzureCloud(String id, String subscriptionId, String serviceManagementCert, String serviceManagementURL,
String maxVirtualMachinesLimit, List<AzureSlaveTemplate> instTemplates, String fileName, String fileData) {
super(Constants.AZURE_CLOUD_PREFIX+subscriptionId);
this.subscriptionId = subscriptionId;
this.serviceManagementCert = serviceManagementCert;
if (AzureUtil.isNull(serviceManagementURL)) {
this.serviceManagementURL = Constants.DEFAULT_MANAGEMENT_URL;
} else {
this.serviceManagementURL = serviceManagementURL;
}
if (AzureUtil.isNull(maxVirtualMachinesLimit) || !maxVirtualMachinesLimit.matches(Constants.REG_EX_DIGIT)) {
this.maxVirtualMachinesLimit = Constants.DEFAULT_MAX_VM_LIMIT;
} else {
this.maxVirtualMachinesLimit = Integer.parseInt(maxVirtualMachinesLimit);
}
if (instTemplates == null) {
this.instTemplates = Collections.emptyList();
} else {
this.instTemplates = instTemplates;
}
readResolve();
}
protected Object readResolve() {
for (AzureSlaveTemplate template : instTemplates) {
template.azureCloud = this;
}
return this;
}
public boolean canProvision(Label label) {
LOGGER.info("Azurecloud: canProvision: called for AzureCloud with label "+label.getDisplayName());
AzureSlaveTemplate template = getAzureSlaveTemplate(label);
// return false if there is no template
if (template == null) {
return false;
} else if (template.getTemplateStatus().equalsIgnoreCase(Constants.TEMPLATE_STATUS_DISBALED)) {
LOGGER.info("Azurecloud: canProvision: template "+template.getTemplateName() +
" is marked has disabled, cannot provision slaves");
return false;
} else {
return true;
}
}
public String getSubscriptionId() {
return subscriptionId;
}
public String getServiceManagementCert() {
return serviceManagementCert;
}
public String getServiceManagementURL() {
return serviceManagementURL;
}
public int getMaxVirtualMachinesLimit() {
return maxVirtualMachinesLimit;
}
public String getPassPhrase() {
return passPhrase;
}
/** Returns slave template associated with the label */
public AzureSlaveTemplate getAzureSlaveTemplate(Label label) {
for (AzureSlaveTemplate slaveTemplate : instTemplates) {
if (label.matches(slaveTemplate.getLabelDataSet())) {
return slaveTemplate;
}
}
return null;
}
/** Returns slave template associated with the name */
public AzureSlaveTemplate getAzureSlaveTemplate(String name) {
if (AzureUtil.isNull(name)) {
return null;
}
for (AzureSlaveTemplate slaveTemplate : instTemplates) {
if (name.equalsIgnoreCase(slaveTemplate.getTemplateName())) {
return slaveTemplate;
}
}
return null;
}
public Collection<PlannedNode> provision(Label label, int workLoad) {
LOGGER.info("Azure Cloud: provision: start for label " + label+" workLoad "+workLoad);
final AzureSlaveTemplate slaveTemplate = getAzureSlaveTemplate(label);
List<PlannedNode> plannedNodes = new ArrayList<PlannedNode>();
while (workLoad > 0) {
// Verify template
try {
LOGGER.info("Azure Cloud: provision: Verifying template " + slaveTemplate.getTemplateName());
List<String> errors = slaveTemplate.verifyTemplate();
if (errors.size() > 0 ) {
LOGGER.info("Azure Cloud: provision: template " + slaveTemplate.getTemplateName() + "has validation errors , cannot"
+" provision slaves with this configuration "+errors);
slaveTemplate.setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED);
slaveTemplate.setTemplateStatusDetails("Validation Error: Validation errors in template \n" + " Root cause: "+errors);
// Register template for periodic check so that jenkins can make template active if validation errors are corrected
AzureTemplateMonitorTask.registerTemplate(slaveTemplate);
break;
} else {
LOGGER.info("Azure Cloud: provision: template " + slaveTemplate.getTemplateName() + " has no validation errors");
}
} catch (Exception e) {
LOGGER.severe("Azure Cloud: provision: Exception occured while validating template"+e);
slaveTemplate.setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED);
slaveTemplate.setTemplateStatusDetails("Validation Error: Exception occured while validating template "+e.getMessage());
// Register template for periodic check so that jenkins can make template active if validation errors are corrected
AzureTemplateMonitorTask.registerTemplate(slaveTemplate);
break;
}
plannedNodes.add(new PlannedNode(slaveTemplate.getTemplateName(),
Computer.threadPoolForRemoting.submit(new Callable<Node>() {
public Node call() throws Exception {
@SuppressWarnings("deprecation")
AzureSlave slave = slaveTemplate.provisionSlave(new StreamTaskListener(System.out));
// Get virtual machine properties
LOGGER.info("Azure Cloud: provision: Getting virtual machine properties for slave "+slave.getNodeName()
+ " with OS "+slave.getOsType());
slaveTemplate.setVirtualMachineDetails(slave);
if (slave.getSlaveLaunchMethod().equalsIgnoreCase("SSH")) {
slaveTemplate.waitForReadyRole(slave);
LOGGER.info("Azure Cloud: provision: Waiting for ssh server to comeup");
Thread.sleep(2 * 60 * 1000);
LOGGER.info("Azure Cloud: provision: ssh server may be up by this time");
LOGGER.info("Azure Cloud: provision: Adding slave to azure nodes ");
Hudson.getInstance().addNode(slave);
slave.toComputer().connect(false).get();
} else if (slave.getSlaveLaunchMethod().equalsIgnoreCase("JNLP")) {
LOGGER.info("Azure Cloud: provision: Checking for slave status");
slaveTemplate.waitForReadyRole(slave);
Hudson.getInstance().addNode(slave);
// Wait until node is online
waitUntilOnline(slave);
}
return slave;
}
}), slaveTemplate.getNoOfParallelJobs()));
// Decrement workload
workLoad -= slaveTemplate.getNoOfParallelJobs();
}
return plannedNodes;
}
/** this methods wait for node to be available */
private void waitUntilOnline(final AzureSlave slave) {
LOGGER.info("Azure Cloud: waitUntilOnline: for slave "+slave.getDisplayName());
ExecutorService executorService = Executors.newCachedThreadPool();
Callable<String> callableTask = new Callable<String>() {
public String call() {
try {
slave.toComputer().waitUntilOnline();
} catch (InterruptedException e) {
// just ignore
}
return "success";
}
};
Future<String> future = executorService.submit(callableTask);
try {
// 30 minutes is decent time for the node to be alive
String result = future.get(30, TimeUnit.MINUTES);
LOGGER.info("Azure Cloud: waitUntilOnline: node is alive , result "+result);
} catch (TimeoutException ex) {
LOGGER.info("Azure Cloud: waitUntilOnline: Got TimeoutException "+ex);
markSlaveForDeletion(slave, Constants.JNLP_POST_PROV_LAUNCH_FAIL);
} catch (InterruptedException ex) {
LOGGER.info("Azure Cloud: InterruptedException: Got TimeoutException "+ex);
markSlaveForDeletion(slave, Constants.JNLP_POST_PROV_LAUNCH_FAIL);
} catch (ExecutionException ex) {
LOGGER.info("Azure Cloud: ExecutionException: Got TimeoutException "+ex);
markSlaveForDeletion(slave, Constants.JNLP_POST_PROV_LAUNCH_FAIL);
} finally {
future.cancel(true);
executorService.shutdown();
}
}
private static void markSlaveForDeletion(AzureSlave slave, String message) {
slave.setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED, message);
slave.toComputer().setTemporarilyOffline(true, OfflineCause.create(Messages._Slave_Failed_To_Connect()));
slave.setDeleteSlave(true);
}
public void doProvision(StaplerRequest req, StaplerResponse rsp, @QueryParameter String templateName) throws Exception {
LOGGER.info("Azure Cloud: doProvision: start name = "+templateName);
checkPermission(PROVISION);
if (AzureUtil.isNull(templateName)) {
sendError("Azure Cloud: doProvision: Azure Slave template name is missing", req, rsp);
return;
}
final AzureSlaveTemplate slaveTemplate = getAzureSlaveTemplate(templateName);
if (slaveTemplate == null) {
sendError("Azure Cloud: doProvision: Azure Slave template configuration is not there for : " + templateName, req, rsp);
return;
}
// 1. Verify template
try {
LOGGER.info("Azure Cloud: doProvision: Verifying template " + slaveTemplate.getTemplateName());
List<String> errors = slaveTemplate.verifyTemplate();
if (errors.size() > 0 ) {
LOGGER.info("Azure Cloud: doProvision: template " + slaveTemplate.getTemplateName() + " has validation errors , cannot"
+" provision slaves with this configuration "+errors);
sendError("template " + slaveTemplate.getTemplateName() + "has validation errors "+errors, req, rsp);
return;
} else {
LOGGER.info("Azure Cloud: provision: template " + slaveTemplate.getTemplateName() + "has no validation errors");
}
} catch (Exception e) {
LOGGER.severe("Azure Cloud: provision: Exception occured while validating template "+e);
sendError("Exception occured while validating template "+e);
return;
}
LOGGER.severe("Azure Cloud: doProvision: creating slave ");
Computer.threadPoolForRemoting.submit(new Callable<Node>() {
public Node call() throws Exception {
@SuppressWarnings("deprecation")
AzureSlave slave = slaveTemplate.provisionSlave(new StreamTaskListener(System.out));
// Get virtual machine properties
LOGGER.info("Azure Cloud: provision: Getting virtual machine properties for slave "+slave.getNodeName()
+ " with OS "+slave.getOsType());
slaveTemplate.setVirtualMachineDetails(slave);
if (slave.getSlaveLaunchMethod().equalsIgnoreCase("SSH")) {
slaveTemplate.waitForReadyRole(slave);
LOGGER.info("Azure Cloud: provision: Waiting for ssh server to comeup");
Thread.sleep(2 * 60 * 1000);
LOGGER.info("Azure Cloud: provision: ssh server may be up by this time");
LOGGER.info("Azure Cloud: provision: Adding slave to azure nodes ");
Hudson.getInstance().addNode(slave);
slave.toComputer().connect(false).get();
} else if (slave.getSlaveLaunchMethod().equalsIgnoreCase("JNLP")) {
LOGGER.info("Azure Cloud: provision: Checking for slave status");
slaveTemplate.waitForReadyRole(slave);
Hudson.getInstance().addNode(slave);
// Wait until node is online
waitUntilOnline(slave);
}
return slave;
}
});
rsp.sendRedirect2(req.getContextPath() + "/computer/");
return;
}
public List<AzureSlaveTemplate> getInstTemplates() {
return Collections.unmodifiableList(instTemplates);
}
@Extension
public static class DescriptorImpl extends Descriptor<Cloud> {
public String getDisplayName() {
return Constants.AZURE_CLOUD_DISPLAY_NAME;
}
public String getDefaultserviceManagementURL() {
return Constants.DEFAULT_MANAGEMENT_URL;
}
public int getDefaultMaxVMLimit() {
return Constants.DEFAULT_MAX_VM_LIMIT;
}
public FormValidation doVerifyConfiguration(@QueryParameter String subscriptionId, @QueryParameter String serviceManagementCert,
@QueryParameter String passPhrase, @QueryParameter String serviceManagementURL) {
if (AzureUtil.isNull(subscriptionId)) {
return FormValidation.error("Error: Subscription ID is missing");
}
if (AzureUtil.isNull(serviceManagementCert)) {
return FormValidation.error("Error: Management service certificate is missing");
}
if (AzureUtil.isNull(serviceManagementURL)) {
serviceManagementURL = Constants.DEFAULT_MANAGEMENT_URL;
}
String response = AzureManagementServiceDelegate.verifyConfiguration(subscriptionId, serviceManagementCert, passPhrase, serviceManagementURL);
if (response.equalsIgnoreCase("Success")) {
return FormValidation.ok(Messages.Azure_Config_Success());
} else {
return FormValidation.error(response);
}
}
}
}

View File

@ -0,0 +1,100 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.util.logging.Logger;
import org.kohsuke.stapler.DataBoundConstructor;
import com.microsoftopentechnologies.azure.util.Constants;
import hudson.model.Descriptor;
import hudson.slaves.RetentionStrategy;
import hudson.util.TimeUnit2;
public class AzureCloudRetensionStrategy extends RetentionStrategy<AzureComputer> {
public final int idleTerminationMinutes;
private static final Logger LOGGER = Logger.getLogger(AzureManagementServiceDelegate.class.getName());
@DataBoundConstructor
public AzureCloudRetensionStrategy(int idleTerminationMinutes) {
this.idleTerminationMinutes = idleTerminationMinutes;
}
public long check(AzureComputer slaveNode) {
// if idleTerminationMinutes is zero then it means that never terminate the slave instance
if (idleTerminationMinutes == 0) {
LOGGER.info("AzureCloudRetensionStrategy: check: Idle termination time for node is zero , "
+ "no need to terminate the slave "+slaveNode.getDisplayName());
return 1;
}
// Do we need to check about slave status?
if (slaveNode.isIdle()) {
if (idleTerminationMinutes > 0) {
final long idleMilliseconds = System.currentTimeMillis() - slaveNode.getIdleStartMilliseconds();
if (idleMilliseconds > TimeUnit2.MINUTES.toMillis(idleTerminationMinutes)) {
// close channel
try {
slaveNode.setAcceptingTasks(false);
if (slaveNode.getChannel() != null ) {
slaveNode.getChannel().close();
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.info("AzureCloudRetensionStrategy: check: exception occured while closing channel for: "+slaveNode.getName());
}
LOGGER.info("AzureCloudRetensionStrategy: check: Idle timeout reached for slave: "+slaveNode.getName());
int retryCount = 0;
boolean successfull = false;
// Retrying for 30 times with 30 seconds wait time between each retry
while (retryCount < 30 && !successfull) {
try {
slaveNode.getNode().idleTimeout();
successfull = true;
} catch (Exception e) {
retryCount++;
LOGGER.info("AzureCloudRetensionStrategy: check: Exception occured while calling timeout on node , \n"
+ "Will retry again after 30 seconds. Current retry count "+retryCount + "\n"
+ "Error code "+e.getMessage());
// We won't get exception for RNF , so for other exception types we can retry
try {
Thread.sleep(30 * 1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
}
}
return 1;
}
public void start(AzureComputer azureComputer) {
//TODO: check when this method is getting called and add code accordingly
LOGGER.info("AzureCloudRetensionStrategy: start: azureComputer name "+azureComputer.getDisplayName());
azureComputer.connect(false);
}
public static class DescriptorImpl extends Descriptor<RetentionStrategy<?>> {
public String getDisplayName() {
return Constants.AZURE_CLOUD_DISPLAY_NAME;
}
}
}

View File

@ -0,0 +1,68 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.io.IOException;
import java.util.logging.Logger;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import hudson.slaves.AbstractCloudComputer;
import hudson.slaves.OfflineCause;
public class AzureComputer extends AbstractCloudComputer<AzureSlave> {
private static final Logger LOGGER = Logger.getLogger(AzureComputer.class.getName());
public AzureComputer(AzureSlave slave) {
super(slave);
}
public AzureSlave getNode() {
return (AzureSlave)super.getNode();
}
public HttpResponse doDoDelete() throws IOException {
LOGGER.info("AzureComputer: doDoDelete called for slave "+getNode().getNodeName());
setTemporarilyOffline(true, OfflineCause.create(Messages._Delete_Slave()));
getNode().setDeleteSlave(true);
try {
deleteSlave();
} catch(Exception e) {
LOGGER.info("AzureComputer: doDoDelete: Exception occured while deleting slave "+e);
throw new IOException("Error occured while deleting node, jenkins will try to clean up node automatically after some time. "
+ " \n Root cause: "+e.getMessage());
}
return new HttpRedirect("..");
}
public void deleteSlave() throws Exception, InterruptedException {
LOGGER.info("AzureComputer : deleteSlave: Deleting " + getName() + " slave");
AzureSlave slave = getNode();
if (slave.getChannel() != null) {
slave.getChannel().close();
}
try {
slave.deprovision();
} catch (Exception e) {
LOGGER.severe("AzureComputer : Exception occured while deleting " + getName() + " slave");
LOGGER.severe("Root cause " + e.getMessage());
throw e;
}
}
}

View File

@ -0,0 +1,298 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import org.kohsuke.stapler.DataBoundConstructor;
import com.microsoftopentechnologies.azure.util.Constants;
import com.microsoftopentechnologies.azure.remote.AzureSSHLauncher;
import hudson.Extension;
import hudson.model.Hudson;
import hudson.model.TaskListener;
import hudson.model.Descriptor.FormException;
import hudson.slaves.AbstractCloudComputer;
import hudson.slaves.AbstractCloudSlave;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.NodeProperty;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.RetentionStrategy;
public class AzureSlave extends AbstractCloudSlave {
private static final long serialVersionUID = 1L;
private String cloudName;
private String adminUserName;
private String sshPrivateKey;
private String sshPassPhrase;
private String adminPassword;
private String jvmOptions;
private boolean shutdownOnIdle;
private String cloudServiceName;
private int retentionTimeInMin;
private String slaveLaunchMethod;
private String initScript;
private String deploymentName;
private String osType;
// set during post create step
private String publicDNSName;
private int sshPort;
private Mode mode;
private String subscriptionID;
private String managementCert;
private String passPhrase;
private String managementURL;
private String templateName;
private boolean deleteSlave;
private static final Logger LOGGER = Logger.getLogger(AzureSlave.class.getName());
@DataBoundConstructor
public AzureSlave(String name, String templateName, String nodeDescription, String osType, String remoteFS, int numExecutors, Mode mode, String labelString,
ComputerLauncher launcher, RetentionStrategy<AzureComputer> retentionStrategy, List<? extends NodeProperty<?>> nodeProperties,
String cloudName, String adminUserName, String sshPrivateKey, String sshPassPhrase, String adminPassword, String jvmOptions,
boolean shutdownOnIdle, String cloudServiceName, String deploymentName, int retentionTimeInMin, String initScript,
String subscriptionID, String managementCert, String passPhrase, String managementURL, String slaveLaunchMethod, boolean deleteSlave) throws FormException, IOException {
super(name, nodeDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, nodeProperties);
this.cloudName = cloudName;
this.templateName = templateName;
this.adminUserName = adminUserName;
this.sshPrivateKey = sshPrivateKey;
this.sshPassPhrase = sshPassPhrase;
this.adminPassword = adminPassword;
this.jvmOptions = jvmOptions;
this.shutdownOnIdle = shutdownOnIdle;
this.cloudServiceName = cloudServiceName;
this.deploymentName = deploymentName;
this.retentionTimeInMin = retentionTimeInMin;
this.initScript = initScript;
this.osType = osType;
this.mode = mode;
this.subscriptionID = subscriptionID;
this.managementCert = managementCert;
this.passPhrase = passPhrase;
this.managementURL = managementURL;
this.slaveLaunchMethod = slaveLaunchMethod;
this.deleteSlave = deleteSlave;
}
public AzureSlave(String name, String templateName, String nodeDescription, String osType, String remoteFS, int numExecutors, Mode mode, String labelString,
String cloudName, String adminUserName, String sshPrivateKey, String sshPassPhrase, String adminPassword, String jvmOptions,
boolean shutdownOnIdle, String cloudServiceName, String deploymentName, int retentionTimeInMin, String initScript,
String subscriptionID, String managementCert, String passPhrase, String managementURL, String slaveLaunchMethod, boolean deleteSlave) throws FormException, IOException {
this(name, templateName, nodeDescription, osType, remoteFS, numExecutors, mode, labelString,
slaveLaunchMethod.equalsIgnoreCase("SSH")? osType.equalsIgnoreCase("Windows")? new AzureSSHLauncher():new AzureSSHLauncher() : new JNLPLauncher(),
new AzureCloudRetensionStrategy(retentionTimeInMin), Collections.<NodeProperty<?>> emptyList(), cloudName, adminUserName,
sshPrivateKey, sshPassPhrase, adminPassword, jvmOptions, shutdownOnIdle, cloudServiceName, deploymentName, retentionTimeInMin, initScript,
subscriptionID, managementCert, passPhrase, managementURL, slaveLaunchMethod, deleteSlave);
this.cloudName = cloudName;
this.templateName = templateName;
this.adminUserName = adminUserName;
this.sshPrivateKey = sshPrivateKey;
this.sshPassPhrase = sshPassPhrase;
this.adminPassword = adminPassword;
this.jvmOptions = jvmOptions;
this.shutdownOnIdle = shutdownOnIdle;
this.cloudServiceName = cloudServiceName;
this.deploymentName = deploymentName;
this.retentionTimeInMin = retentionTimeInMin;
this.initScript = initScript;
this.osType = osType;
this.mode = mode;
this.subscriptionID = subscriptionID;
this.managementCert = managementCert;
this.passPhrase = passPhrase;
this.managementURL = managementURL;
this.deleteSlave = deleteSlave;
}
public String getCloudName() {
return cloudName;
}
public Mode getMode() {
return mode;
}
public String getAdminUserName() {
return adminUserName;
}
public String getSubscriptionID() {
return subscriptionID;
}
public String getManagementCert() {
return managementCert;
}
public String getPassPhrase() {
return passPhrase;
}
public String getManagementURL() {
return managementURL;
}
public String getSshPrivateKey() {
return sshPrivateKey;
}
public String getOsType() {
return osType;
}
public String getSshPassPhrase() {
return sshPassPhrase;
}
public String getCloudServiceName() {
return cloudServiceName;
}
public String getDeploymentName() {
return deploymentName;
}
public String getAdminPassword() {
return adminPassword;
}
public boolean isDeleteSlave() {
return deleteSlave;
}
public void setDeleteSlave(boolean deleteSlave) {
this.deleteSlave = deleteSlave;
}
public String getJvmOptions() {
return jvmOptions;
}
public boolean isShutdownOnIdle() {
return shutdownOnIdle;
}
public void setShutdownOnIdle(boolean shutdownOnIdle) {
this.shutdownOnIdle = shutdownOnIdle;
}
public String getPublicDNSName() {
return publicDNSName;
}
public void setPublicDNSName(String publicDNSName) {
this.publicDNSName = publicDNSName;
}
public int getSshPort() {
return sshPort;
}
public void setSshPort(int sshPort) {
this.sshPort = sshPort;
}
public int getRetentionTimeInMin() {
return retentionTimeInMin;
}
public String getInitScript() {
return initScript;
}
public String getSlaveLaunchMethod() {
return slaveLaunchMethod;
}
public String getTemplateName() {
return templateName;
}
public void setTemplateName(String templateName) {
this.templateName = templateName;
}
protected void _terminate(TaskListener arg0) throws IOException, InterruptedException {
//TODO: Check when this method is getting called and code accordingly
LOGGER.info("AzureSlave: _terminate: called for slave "+getNodeName());
}
public AbstractCloudComputer<AzureSlave> createComputer() {
LOGGER.info("AzureSlave: createComputer: start for slave "+this.getDisplayName());
return new AzureComputer(this);
}
public void idleTimeout() throws Exception {
if (shutdownOnIdle) {
LOGGER.info("AzureSlave: idleTimeout: shutdownOnIdle is true, shutting down slave "+this.getDisplayName());
AzureManagementServiceDelegate.shutdownVirtualMachine(this);
setDeleteSlave(false);
} else {
LOGGER.info("AzureSlave: idleTimeout: shutdownOnIdle is false, deleting slave "+this.getDisplayName());
setDeleteSlave(true);
AzureManagementServiceDelegate.terminateVirtualMachine(this, true);
Hudson.getInstance().removeNode(this);
}
}
public AzureCloud getCloud() {
return (AzureCloud) Hudson.getInstance().getCloud(cloudName);
}
public void deprovision() throws Exception {
LOGGER.info("AzureSlave: deprovision: Deprovision called for slave "+this.getDisplayName());
AzureManagementServiceDelegate.terminateVirtualMachine(this, true);
setDeleteSlave(true);
Hudson.getInstance().removeNode(this);
}
public void setTemplateStatus(String templateStatus, String templateStatusDetails) {
AzureCloud azureCloud = getCloud();
AzureSlaveTemplate slaveTemplate = azureCloud.getAzureSlaveTemplate(templateName);
slaveTemplate.setTemplateStatus(templateStatus);
slaveTemplate.setTemplateStatusDetails(templateStatusDetails);
}
public String toString() {
return "AzureSlave [cloudName=" + cloudName + ", adminUserName="
+ adminUserName + ", jvmOptions=" + jvmOptions
+ ", shutdownOnIdle=" + shutdownOnIdle + ", cloudServiceName="
+ cloudServiceName + ", retentionTimeInMin="
+ retentionTimeInMin + ", deploymentName=" + deploymentName
+ ", osType=" + osType + ", publicDNSName=" + publicDNSName
+ ", sshPort=" + sshPort + ", mode=" + mode
+ ", subscriptionID=" + subscriptionID + ", passPhrase=" + passPhrase
+ ", managementURL=" + managementURL + ", deleteSlave="
+ deleteSlave + "]";
}
@Extension
public static final class AzureSlaveDescriptor extends SlaveDescriptor {
public String getDisplayName() {
return Constants.AZURE_SLAVE_DISPLAY_NAME;
}
public boolean isInstantiable() {
return false;
}
}
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.io.IOException;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import hudson.Extension;
import hudson.model.AsyncPeriodicWork;
import hudson.model.TaskListener;
import hudson.model.Computer;
@Extension
public final class AzureSlaveCleanUpTask extends AsyncPeriodicWork {
private static final Logger LOGGER = Logger.getLogger(AzureSlaveCleanUpTask.class.getName());
public AzureSlaveCleanUpTask() {
super("Azure slave clean task");
}
public void execute(TaskListener arg0) throws IOException, InterruptedException {
LOGGER.info("AzureSlaveCleanUpTask: execute: start");
for (Computer computer : Jenkins.getInstance().getComputers()) {
if (computer instanceof AzureComputer) {
AzureComputer azureComputer = (AzureComputer)computer;
AzureSlave slaveNode = azureComputer.getNode();
try {
if (azureComputer.isOffline()) {
if (!slaveNode.isDeleteSlave()) {
// Find out if node exists in azure , if not continue with delete else donot delete node
// although it is offline. May be JNLP or SSH launch is in progress
if(AzureManagementServiceDelegate.isVirtualMachineExists(slaveNode)) {
LOGGER.info("AzureSlaveCleanUpTask: execute: VM "+slaveNode.getDisplayName()+" exists in cloud");
continue;
}
}
int retryCount = 0;
boolean successfull = false;
// Retrying for 30 times with 30 seconds wait time between each retry
while (retryCount < 30 && !successfull) {
try {
slaveNode.idleTimeout();
successfull = true;
} catch (Exception e) {
retryCount++;
LOGGER.info("AzureSlaveCleanUpTask: execute: Exception occured while calling timeout on node , \n"
+ "Will retry again after 30 seconds. Current retry count "+retryCount + "\n"
+ "Error code "+e.getMessage());
// We won't get exception for RNF , so for other exception types we can retry
try {
Thread.sleep(30 * 1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
Jenkins.getInstance().removeNode(slaveNode);
}
} catch (Exception e) {
LOGGER.severe("AzureSlaveCleanUpTask: execute: failed to remove node " +e);
}
}
}
LOGGER.info("AzureSlaveCleanUpTask: execute: end");
}
public long getRecurrencePeriod() {
// Every 5 minutes
return 5 * 60 * 1000;
}
}

View File

@ -0,0 +1,124 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.io.IOException;
import java.util.logging.Logger;
import org.kohsuke.stapler.DataBoundConstructor;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.ListBoxModel;
import com.microsoftopentechnologies.azure.Messages;
public class AzureSlavePostBuildAction extends Recorder {
/** Windows Azure Storage Account Name. */
private String slavePostBuildAction;
public static final Logger LOGGER = Logger.getLogger(AzureSlavePostBuildAction.class.getName());
@DataBoundConstructor
public AzureSlavePostBuildAction(final String slavePostBuildAction) {
super();
this.slavePostBuildAction = slavePostBuildAction;
}
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
BuildListener listener) throws InterruptedException, IOException {
if (build.getResult() != Result.SUCCESS) {
LOGGER.info("AzureSlavePostBuildAction: perform: build is not successful , taking post build action "+slavePostBuildAction+" for slave ");
Node node = Computer.currentComputer().getNode();
int retryCount = 0;
boolean successfull = false;
// Retrying for 30 times with 30 seconds wait time between each retry
while (retryCount < 30 && !successfull) {
try {
//check if node is instance of azure slave
if (node instanceof AzureSlave) {
AzureSlave slave = (AzureSlave)node;
if (slave.getChannel() != null) {
slave.getChannel().close();
}
if (Messages.Shutdown_Slave_If_Not_Success().equalsIgnoreCase(slavePostBuildAction)) {
slave.setShutdownOnIdle(true);
slave.idleTimeout();
} else {
slave.setShutdownOnIdle(false);
slave.idleTimeout();
}
}
successfull = true;
} catch (Exception e) {
retryCount++;
LOGGER.info("AzureSlavePostBuildAction: perform: Exception occured while " + slavePostBuildAction + "\n"
+ "Will retry again after 30 seconds. Current retry count "+retryCount + "\n"
+ "Error code "+e.getMessage());
// We won't get exception for RNF , so for other exception types we can retry
try {
Thread.sleep(30 * 1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
return true;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.STEP;
}
@Extension
public static final class AzureSlavePostBuildDescriptor extends
BuildStepDescriptor<Publisher> {
public boolean isApplicable(Class<? extends AbstractProject> arg0) {
return true;
}
public ListBoxModel doFillSlavePostBuildActionItems() {
ListBoxModel model = new ListBoxModel();
model.add(Messages.Shutdown_Slave_If_Not_Success());
model.add(Messages.Delete_Slave_If_Not_Success());
return model;
}
@Override
public String getDisplayName() {
// TODO Auto-generated method stub
return Messages.Azure_Slave_Post_Build_Action();
}
}
}

View File

@ -0,0 +1,671 @@
/*
Copyright 2014 Microsoft Open Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.microsoftopentechnologies.azure;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import com.microsoft.windowsazure.Configuration;
import com.microsoft.windowsazure.management.compute.ComputeManagementClient;
import com.microsoft.windowsazure.management.compute.models.DeploymentSlot;
import com.microsoftopentechnologies.azure.util.AzureUtil;
import com.microsoftopentechnologies.azure.util.Constants;
import hudson.Extension;
import hudson.RelativePath;
import hudson.model.Describable;
import hudson.model.TaskListener;
import hudson.model.Descriptor;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.labels.LabelAtom;
import hudson.util.ComboBoxModel;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
/**
* This class defines the configuration of Azure instance templates
* @author Suresh Nallamilli
*
*/
public class AzureSlaveTemplate implements Describable<AzureSlaveTemplate> {
// General Configuration
private String templateName;
private String templateDesc;
private String labels;
private String location;
private String virtualMachineSize;
private String storageAccountName;
private int noOfParallelJobs;
private Node.Mode useSlaveAlwaysIfAvail;
private boolean shutdownOnIdle;
// Image Configuration
private String imageIdOrFamily;
private String slaveLaunchMethod;
private String initScript;
private String adminUserName;
private String adminPassword;
private String slaveWorkSpace;
private int retentionTimeInMin;
private String jvmOptions;
private String cloudServiceName;
private String templateStatus;
private String templateStatusDetails;
public transient AzureCloud azureCloud;
private transient Set<LabelAtom> labelDataSet;
private static final Logger LOGGER = Logger.getLogger(AzureSlaveTemplate.class.getName());
@DataBoundConstructor
public AzureSlaveTemplate(String templateName, String templateDesc, String labels, String location, String virtualMachineSize,
String storageAccountName, String noOfParallelJobs, Node.Mode useSlaveAlwaysIfAvail, String imageIdOrFamily, String slaveLaunchMethod,
String initScript, String adminUserName, String adminPassword, String slaveWorkSpace, String jvmOptions, String cloudServiceName,
String retentionTimeInMin, String templateStatus, String templateStatusDetails) {
this.templateName = templateName;
this.templateDesc = templateDesc;
this.labels = labels;
this.location = location;
this.virtualMachineSize = virtualMachineSize;
this.storageAccountName = storageAccountName;
if (AzureUtil.isNull(noOfParallelJobs) || !noOfParallelJobs.matches(Constants.REG_EX_DIGIT) || noOfParallelJobs.trim().equals("0")) {
this.noOfParallelJobs = 1;
} else {
this.noOfParallelJobs = Integer.parseInt(noOfParallelJobs);
}
this.useSlaveAlwaysIfAvail = useSlaveAlwaysIfAvail;
this.shutdownOnIdle = false;
this.imageIdOrFamily = imageIdOrFamily;
this.initScript = initScript;
this.slaveLaunchMethod = slaveLaunchMethod;
this.adminUserName = adminUserName;
this.adminPassword = adminPassword;
this.slaveWorkSpace = slaveWorkSpace;
this.jvmOptions = jvmOptions;
if (AzureUtil.isNull(retentionTimeInMin) || !retentionTimeInMin.matches(Constants.REG_EX_DIGIT)) {
this.retentionTimeInMin = Constants.DEFAULT_IDLE_TIME;
} else {
this.retentionTimeInMin = Integer.parseInt(retentionTimeInMin);
}
this.cloudServiceName = cloudServiceName;
this.templateStatus = templateStatus;
if(templateStatus.equalsIgnoreCase(Constants.TEMPLATE_STATUS_ACTIVE)) {