From 6b53a4e3a330fa855e8c5a9c598110fb5385f719 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Mon, 12 Sep 2016 16:20:16 -0700 Subject: [PATCH] Azure RM model updates * Make the resource group configurable * Update SDK to 0.9.4 * Fix: Token expiration was incorrectly calculated (API is named a bit oddly). * Fix: Token expiration is in seconds. * Change VM/deployment names to ones that are valid * Fix location + VM sizes It appears that that the location and VM size APIs do not currently support the AAD based authentication. The certificate is required. Rather than re-introduce the cert for just this limited UI scenario, I have decided to hard-code the list based on the returned info from current Azure. This is a decent short term solution, since a move to the 1.0.0 API (when released) will require that this code be changed anyway and presumably this problem should be fixed for good at that point. * Proper handling for custom image URIs * Asynchronous verification of the subscription info * Asynchronous verification of the azure templates * Asynchronous provisioning * Clean up resources after unsuccessful provisioning * Lots of logging updates * Fix: SSH launcher - Get channels before connection for exec channels Getting the channels after connection introduces a race where we could potentially fail to read from the input streams if they were obtained from the channel after the connection had completed. * Add diagnostics to image verification messages * Asynchronous deletion via UI * Update version of Jsch * Temporarily disable image verification for reference images * Allow for initialization scripts to be run as root * Cleanup of slaves now works properly * Doesn't clean up when node is taken offline by user * Post build tasks won't kill other runs or show errors in the job logging * Retention strategy now for online nodes, cleanup for offline nodes * Nodes that are marked to shut down on idle can now be restarted properly (before too many could be started) * Add option to treat failures of the initialization script as a reason to discard the VM (linux only currently) * Reenable Windows custom script extension for startup * Update documentation with new sample startup scripts Add setAcceptingTasks appropriately doc fixup --- README.md | 69 +- pom.xml | 4 +- .../azure/AzureCloud.java | 466 ++++++--- .../azure/AzureCloudRetensionStrategy.java | 87 +- .../azure/AzureComputer.java | 105 ++- .../azure/AzureDeploymentInfo.java | 46 + .../azure/AzureManagementServiceDelegate.java | 886 ++++++++++-------- .../azure/AzureSlave.java | 192 +++- .../azure/AzureSlaveCleanUpTask.java | 99 +- .../azure/AzureSlavePostBuildAction.java | 82 +- .../azure/AzureSlaveTemplate.java | 285 +++--- .../azure/AzureTemplateMonitorTask.java | 84 -- .../azure/AzureVerificationTask.java | 312 ++++++ .../azure/ServiceDelegateHelper.java | 3 - .../azure/StorageServiceDelegate.java | 39 +- .../azure/remote/AzureSSHLauncher.java | 128 ++- .../azure/util/AccessToken.java | 4 +- .../azure/util/AzureUtil.java | 110 +++ .../azure/util/CleanUpAction.java | 34 + .../azure/util/Constants.java | 37 +- .../azure/util/TokenCache.java | 36 +- .../azure/AzureCloud/config.jelly | 6 +- .../azure/AzureCloud/config.properties | 3 +- .../azure/AzureSlave/configure-entries.jelly | 1 + .../AzureSlave/configure-entries.properties | 2 +- .../AzureSlavePostBuildAction/config.jelly | 4 +- .../config.properties | 2 +- .../azure/AzureSlaveTemplate/config.jelly | 40 +- .../AzureSlaveTemplate/config.properties | 7 +- .../azure/Messages.properties | 30 +- src/main/resources/customImageTemplate.json | 135 +++ .../customImageTemplateWithScript.json | 170 ++++ .../resources/referenceImageTemplate.json | 137 +++ .../referenceImageTemplateWithScript.json | 172 ++++ src/main/resources/scripts/azure.ps1 | 62 -- src/main/resources/scripts/init.ps1 | 41 + src/main/resources/templateImageValue.json | 135 --- src/main/resources/templateValue.json | 137 --- .../help-doNotUseMachineIfInitFails.html | 4 + .../webapp/help-executeInitScriptAsRoot.html | 4 + src/main/webapp/help-initScript.html | 19 +- .../webapp/help-maxVirtualMachinesLimit.html | 4 +- src/main/webapp/help-templateDisabled.html | 3 + src/main/webapp/help-templateStatus.html | 11 - 44 files changed, 2786 insertions(+), 1451 deletions(-) create mode 100644 src/main/java/com/microsoftopentechnologies/azure/AzureDeploymentInfo.java delete mode 100644 src/main/java/com/microsoftopentechnologies/azure/AzureTemplateMonitorTask.java create mode 100644 src/main/java/com/microsoftopentechnologies/azure/AzureVerificationTask.java create mode 100644 src/main/java/com/microsoftopentechnologies/azure/util/CleanUpAction.java create mode 100644 src/main/resources/customImageTemplate.json create mode 100644 src/main/resources/customImageTemplateWithScript.json create mode 100644 src/main/resources/referenceImageTemplate.json create mode 100644 src/main/resources/referenceImageTemplateWithScript.json delete mode 100644 src/main/resources/scripts/azure.ps1 create mode 100644 src/main/resources/scripts/init.ps1 delete mode 100644 src/main/resources/templateImageValue.json delete mode 100644 src/main/resources/templateValue.json create mode 100644 src/main/webapp/help-doNotUseMachineIfInitFails.html create mode 100644 src/main/webapp/help-executeInitScriptAsRoot.html create mode 100644 src/main/webapp/help-templateDisabled.html delete mode 100644 src/main/webapp/help-templateStatus.html diff --git a/README.md b/README.md index 967b203..c779530 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,59 @@ Refer to 12. For the Init script, provide a script to install at least a Java runtime if the image does not have Java pre-installed. - For the JNLP launch method, the init script must be in PowerShell. - If the init script is expected to take a long time to execute, it is recommended to prepare custom images with the necessary software pre-installed.
+ For the Windows JNLP launch method, the init script must be in PowerShell. + Automatically passed to this script is: + First argument - Jenkins server URL + Second argument - VMName + Third argument - JNLP secret, required if the server has security enabled. + You need to install Java, download the slave jar file from: '[server url]jnlpJars/slave.jar'. + The server url should already have a trailing slash. Then execute the following to connect: + `java.exe -jar [slave jar location] [-secret [client secret if required]] [server url]computer/[vm name]/slave-agent.jnlp` + + Example script + ``` + Set-ExecutionPolicy Unrestricted + $jenkinsServerUrl = $args[0] + $vmName = $args[1] + $secret = $args[2] + + $baseDir = 'C:\Jenkins' + mkdir $baseDir + # Download the JDK + $source = "http://download.oracle.com/otn-pub/java/jdk/7u79-b15/jdk-7u79-windows-x64.exe" + $destination = "$baseDir\jdk.exe" + $client = new-object System.Net.WebClient + $cookie = "oraclelicense=accept-securebackup-cookie" + $client.Headers.Add([System.Net.HttpRequestHeader]::Cookie, $cookie) + $client.downloadFile([string]$source, [string]$destination) + + # Execute the unattended install + $jdkInstallDir=$baseDir + '\jdk\' + $jreInstallDir=$baseDir + '\jre\' + C:\Jenkins\jdk.exe /s INSTALLDIR=$jdkInstallDir /INSTALLDIRPUBJRE=$jdkInstallDir + + $javaExe=$jdkInstallDir + '\bin\java.exe' + $jenkinsSlaveJarUrl = $jenkinsServerUrl + "jnlpJars/slave.jar" + $destinationSlaveJarPath = $baseDir + '\slave.jar' + + # Download the jar file + $client = new-object System.Net.WebClient + $client.DownloadFile($jenkinsSlaveJarUrl, $destinationSlaveJarPath) + + # Calculate the jnlpURL + $jnlpUrl = $jenkinsServerUrl + 'computer/' + $vmName + '/slave-agent.jnlp' + + while ($true) { + try { + # Launch + & $javaExe -jar $destinationSlaveJarPath -secret $secret -jnlpUrl $jnlpUrl -noReconnect + } + catch [System.Exception] { + Write-Output $_.Exception.ToString() + } + sleep 10 + } + ``` For more details about how to prepare custom images, refer to the below links: * [Capture Windows Image](http://azure.microsoft.com/en-us/documentation/articles/virtual-machines-capture-image-windows-server/) @@ -89,22 +140,12 @@ Refer to 1. Configure an Azure profile and Template as per the above instructions. 2. If the init script is expected to take a long time to complete, it is recommended to use a custom-prepared Ubuntu image that has the required software pre-installed, including a Java runtime -3. For platform images, you may specify an Init script as below to install Java, Git and Ant: +3. For platform images, you may specify an Init script as below to install Java (may vary based on OS): ``` #Install Java sudo apt-get -y update - sudo apt-get install -y openjdk-7-jdk - sudo apt-get -y update --fix-missing - sudo apt-get install -y openjdk-7-jdk - - # Install Git - sudo apt-get install -y git - - #Install Ant - sudo apt-get install -y ant - sudo apt-get -y update --fix-missing - sudo apt-get install -y ant + sudo apt-get install -y openjdk-8-jre ``` ## Template configuration for Windows images with launch method JNLP. diff --git a/pom.xml b/pom.xml index eb88eb5..4691c09 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.jenkins-ci.plugins plugin - 1.620 + 1.642.1 azure-slave-plugin @@ -108,7 +108,7 @@ com.jcraft jsch - 0.1.53 + 0.1.54 diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureCloud.java b/src/main/java/com/microsoftopentechnologies/azure/AzureCloud.java index 0e300c2..5123c87 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureCloud.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureCloud.java @@ -18,9 +18,13 @@ package com.microsoftopentechnologies.azure; import com.microsoft.azure.management.compute.models.VirtualMachineGetResponse; import com.microsoft.azure.management.resources.ResourceManagementClient; import com.microsoft.azure.management.resources.ResourceManagementService; +import com.microsoft.azure.management.resources.models.DeploymentGetResult; import com.microsoft.azure.management.resources.models.DeploymentOperation; import com.microsoft.azure.management.resources.models.ProvisioningState; import com.microsoft.windowsazure.Configuration; +import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; +import com.microsoftopentechnologies.azure.util.AzureUtil; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -69,8 +73,22 @@ public class AzureCloud extends Cloud { private final String serviceManagementURL; private final int maxVirtualMachinesLimit; + + private final String resourceGroupName; private final List instTemplates; + + // True if the subscription has been verified. + // False otherwise. + private boolean configurationValid; + + // True if initial verification was queued for this cloud. + // Set on either: construction or initial canProvision if + // not already set. + private transient boolean initialVerificationQueued; + + // Approximate virtual machine count. Updated periodically. + private int approximateVirtualMachineCount; @DataBoundConstructor public AzureCloud( @@ -81,16 +99,16 @@ public class AzureCloud extends Cloud { final String oauth2TokenEndpoint, final String serviceManagementURL, final String maxVirtualMachinesLimit, - final List instTemplates, - final String fileName, - final String fileData) { + final String resourceGroupName, + final List instTemplates) { - super(Constants.AZURE_CLOUD_PREFIX + subscriptionId); + super(AzureUtil.getCloudName(subscriptionId)); this.subscriptionId = subscriptionId; this.clientId = clientId; this.clientSecret = clientSecret; this.oauth2TokenEndpoint = oauth2TokenEndpoint; + this.resourceGroupName = resourceGroupName; this.serviceManagementURL = StringUtils.isBlank(serviceManagementURL) ? Constants.DEFAULT_MANAGEMENT_URL @@ -101,11 +119,37 @@ public class AzureCloud extends Cloud { } else { this.maxVirtualMachinesLimit = Integer.parseInt(maxVirtualMachinesLimit); } - + + this.configurationValid = false; + this.instTemplates = instTemplates == null ? Collections.emptyList() : instTemplates; + readResolve(); + + registerInitialVerificationIfNeeded(); + } + + /** + * Register the initial verification if required + */ + private void registerInitialVerificationIfNeeded() { + if (this.initialVerificationQueued) { + return; + } + // Register the cloud and the templates for verification + AzureVerificationTask.registerCloud(this.name); + // Register all templates. We don't know what happened with them + // when save was hit. + AzureVerificationTask.registerTemplates(this.getInstTemplates()); + // Force the verification task to run if it's not already running. + // Note that early in startup this could return null + if (AzureVerificationTask.get() != null) { + AzureVerificationTask.get().doRun(); + // Set the initial verification as being queued and ready to go. + this.initialVerificationQueued = true; + } } private Object readResolve() { @@ -117,16 +161,33 @@ public class AzureCloud extends Cloud { @Override public boolean canProvision(final Label label) { - final AzureSlaveTemplate template = getAzureSlaveTemplate(label); - // return false if there is no template - if (template == null) { - LOGGER.log(Level.INFO, "Azurecloud: canProvision: template not found for label {0}", label); + if (!configurationValid) { + // The subscription is not verified or is not valid, + // so we can't provision any nodes. + LOGGER.log(Level.INFO, "Azurecloud: canProvision: Subscription not verified, or is invalid, cannot provision"); + registerInitialVerificationIfNeeded(); return false; - } else if (template.getTemplateStatus().equalsIgnoreCase(Constants.TEMPLATE_STATUS_DISBALED)) { + } + + final AzureSlaveTemplate template = getAzureSlaveTemplate(label); + // return false if there is no template for this label. + if (template == null) { + // Avoid logging this, it happens a lot and is just noisy in logs. + return false; + } else if (template.isTemplateDisabled()) { + // Log this. It's not terribly noisy and can be useful LOGGER.log(Level.INFO, "Azurecloud: canProvision: template {0} is marked has disabled, cannot provision slaves", template.getTemplateName()); return false; + } else if (!template.isTemplateVerified()) { + // The template is available, but not verified. It may be queued for + // verification, but ensure that it's added. + LOGGER.log(Level.INFO, + "Azurecloud: canProvision: template {0} is awaiting verification or has failed verification", + template.getTemplateName()); + AzureVerificationTask.registerTemplate(template); + return false; } else { return true; } @@ -155,7 +216,88 @@ public class AzureCloud extends Cloud { public int getMaxVirtualMachinesLimit() { return maxVirtualMachinesLimit; } - + + public String getResourceGroupName() { + return resourceGroupName; + } + + /** + * Returns the current set of templates. + * Required for config.jelly + * @return + */ + public List getInstTemplates() { + return Collections.unmodifiableList(instTemplates); + } + + /** + * Is the configuration set up and verified? + * @return True if the configuration set up and verified, false otherwise. + */ + public boolean isConfigurationValid() { + return configurationValid; + } + + /** + * Set the configuration verification status + * @param isValid True for verified + valid, false otherwise. + */ + public void setConfigurationValid(boolean isValid) { + configurationValid = isValid; + } + + /** + * Retrieves the current approximate virtual machine count + * @return + */ + public int getApproximateVirtualMachineCount() { + synchronized (this) { + return approximateVirtualMachineCount; + } + } + + /** + * Given the number of VMs that are desired, returns the number + * of VMs that can be allocated. + * @param quantityDesired Number that are desired + * @return Number that can be allocated + */ + public int getAvailableVirtualMachineCount(int quantityDesired) { + synchronized (this) { + if (approximateVirtualMachineCount + quantityDesired <= getMaxVirtualMachinesLimit()) { + // Enough available, return the desired quantity + return quantityDesired; + } + else { + // Not enough available, return what we have. Remember we could + // go negative (if for instance another Jenkins instance had + // a higher limit. + return Math.max(0, getMaxVirtualMachinesLimit() - approximateVirtualMachineCount); + } + } + } + + /** + * Adjust the number of currently allocated VMs + * @param delta Number to adjust by. + */ + public void adjustVirtualMachineCount(int delta) { + synchronized (this) { + approximateVirtualMachineCount = Math.max(0, approximateVirtualMachineCount + delta); + } + } + + /** + * Sets the new approximate virtual machine count. This is run by + * the verification task to update the VM count periodically. + * @param newCount + */ + public void setVirtualMachineCount(int newCount) { + synchronized (this) { + approximateVirtualMachineCount = newCount; + } + } + /** * Returns slave template associated with the label. * @@ -163,17 +305,17 @@ public class AzureCloud extends Cloud { * @return */ public AzureSlaveTemplate getAzureSlaveTemplate(final Label label) { - LOGGER.log(Level.INFO, "Retrieving slave template with label {0}", label); + LOGGER.log(Level.FINE, "AzureCloud: getAzureSlaveTemplate: Retrieving slave template with label {0}", label); for (AzureSlaveTemplate slaveTemplate : instTemplates) { - LOGGER.log(Level.INFO, "Found slave template {0}", slaveTemplate.getTemplateName()); + LOGGER.log(Level.FINE, "AzureCloud: getAzureSlaveTemplate: Found slave template {0}", slaveTemplate.getTemplateName()); if (slaveTemplate.getUseSlaveAlwaysIfAvail() == Node.Mode.NORMAL) { if (label == null || label.matches(slaveTemplate.getLabelDataSet())) { - LOGGER.log(Level.INFO, "{0} matches!", slaveTemplate.getTemplateName()); + LOGGER.log(Level.FINE, "AzureCloud: getAzureSlaveTemplate: {0} matches!", slaveTemplate.getTemplateName()); return slaveTemplate; } } else if (slaveTemplate.getUseSlaveAlwaysIfAvail() == Node.Mode.EXCLUSIVE) { if (label != null && label.matches(slaveTemplate.getLabelDataSet())) { - LOGGER.log(Level.INFO, "{0} matches!", slaveTemplate.getTemplateName()); + LOGGER.log(Level.FINE, "AzureCloud: getAzureSlaveTemplate: {0} matches!", slaveTemplate.getTemplateName()); return slaveTemplate; } } @@ -200,148 +342,105 @@ public class AzureCloud extends Cloud { return null; } - public List getInstTemplates() { - return Collections.unmodifiableList(instTemplates); - } - - private boolean verifyTemplate(final AzureSlaveTemplate template) { - boolean isVerified; - try { - LOGGER.log(Level.INFO, "Azure Cloud: provision: Verifying template {0}", template.getTemplateName()); - - final List errors = template.verifyTemplate(); - - isVerified = errors.isEmpty(); - - if (isVerified) { - LOGGER.log(Level.INFO, - "Azure Cloud: provision: template {0} has no validation errors", template.getTemplateName()); - } else { - LOGGER.log(Level.INFO, "Azure Cloud: provision: template {0}" - + " has validation errors , cannot provision slaves with this configuration {1}", - new Object[] { template.getTemplateName(), errors }); - template.handleTemplateStatus("Validation Error: Validation errors in template \n" - + " Root cause: " + errors, FailureStage.VALIDATION, null); - - // Register template for periodic check so that jenkins can make template active if - // validation errors are corrected - if (!Constants.TEMPLATE_STATUS_ACTIVE_ALWAYS.equals(template.getTemplateStatus())) { - AzureTemplateMonitorTask.registerTemplate(template); - } - } - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Azure Cloud: provision: Exception occured while validating template", e); - template.handleTemplateStatus("Validation Error: Exception occured while validating template " - + e.getMessage(), FailureStage.VALIDATION, null); - - // Register template for periodic check so that jenkins can make template active if validation errors - // are corrected - if (!Constants.TEMPLATE_STATUS_ACTIVE_ALWAYS.equals(template.getTemplateStatus())) { - AzureTemplateMonitorTask.registerTemplate(template); - } - isVerified = false; - } - - return isVerified; - } - - private AzureSlave provisionedSlave( + /** + * Once a new deployment is created, construct a new AzureSlave object + * given information about the template + * @param template Template used to create the new slave + * @param vmName Name of the created VM + * @param deploymentName Name of the deployment containing the VM + * @param config Azure configuration. + * @return New slave. Throws otherwise. + * @throws Exception + */ + private AzureSlave createProvisionedSlave( final AzureSlaveTemplate template, - final String prefix, - final int index, - final int expectedVMs, + final String vmName, + final String deploymentName, final Configuration config) throws Exception { final ResourceManagementClient rmc = ResourceManagementService.create(config); - final String vmName = String.format("%s%s%d", template.getTemplateName(), prefix, index); - - int completed = 0; - - AzureSlave slave = null; - + LOGGER.log(Level.INFO, "AzureCloud: createProvisionedSlave: Waiting for deployment to be completed"); + + int triesLeft = 20; do { + triesLeft--; try { Thread.sleep(30 * 1000); } catch (InterruptedException ex) { // ignore } - + final List ops = rmc.getDeploymentOperationsOperations(). - list(Constants.RESOURCE_GROUP_NAME, prefix, null).getOperations(); - - completed = 0; + list(resourceGroupName, deploymentName, null).getOperations(); + for (DeploymentOperation op : ops) { final String resource = op.getProperties().getTargetResource().getResourceName(); final String type = op.getProperties().getTargetResource().getResourceType(); final String state = op.getProperties().getProvisioningState(); - if (ProvisioningState.CANCELED.equals(state) - || ProvisioningState.FAILED.equals(state) - || ProvisioningState.NOTSPECIFIED.equals(state)) { - LOGGER.log(Level.INFO, "Failed({0}): {1}:{2}", new Object[] { state, type, resource }); + if (op.getProperties().getTargetResource().getResourceType().contains("virtualMachine")) { + if (resource.equalsIgnoreCase(vmName)) { + if (ProvisioningState.CANCELED.equals(state) + || ProvisioningState.FAILED.equals(state) + || ProvisioningState.NOTSPECIFIED.equals(state)) { + final String statusCode = op.getProperties().getStatusCode(); + final String statusMessage = op.getProperties().getStatusMessage(); + String finalStatusMessage = statusCode; + if (statusMessage != null) { + finalStatusMessage += " - " + statusMessage; + } - slave = AzureManagementServiceDelegate.parseResponse( - vmName, prefix, template, template.getOsType()); - } else if (ProvisioningState.SUCCEEDED.equals(state)) { - if (op.getProperties().getTargetResource().getResourceType().contains("virtualMachine")) { - if (resource.equalsIgnoreCase(vmName)) { - LOGGER.log(Level.INFO, "VM available: {0}", resource); + throw new AzureCloudException(String.format("AzureCloud: createProvisionedSlave: Deployment %s: %s:%s - %s", new Object[] { state, type, resource, finalStatusMessage })); + } else if (ProvisioningState.SUCCEEDED.equals(state)) { + LOGGER.log(Level.INFO, "AzureCloud: createProvisionedSlave: VM available: {0}", resource); final VirtualMachineGetResponse vm = ServiceDelegateHelper.getComputeManagementClient(config). getVirtualMachinesOperations(). - getWithInstanceView(Constants.RESOURCE_GROUP_NAME, resource); + getWithInstanceView(resourceGroupName, resource); final String osType = vm.getVirtualMachine().getStorageProfile().getOSDisk(). getOperatingSystemType(); - slave = AzureManagementServiceDelegate.parseResponse(vmName, prefix, template, osType); + AzureSlave newSlave = AzureManagementServiceDelegate.parseResponse(vmName, deploymentName, template, osType); + // Set the virtual machine details + AzureManagementServiceDelegate.setVirtualMachineDetails(newSlave, template); + return newSlave; + } + else { + LOGGER.log(Level.INFO, "AzureCloud: createProvisionedSlave: Deployment not yet finished ({0}): {1}:{2}", new Object[] { state, type, resource }); } - - completed++; } - } else { - LOGGER.log(Level.INFO, "To Be Completed({0}): {1}:{2}", new Object[] { state, type, resource }); } } - } while (slave == null && completed < expectedVMs); + } while (triesLeft > 0); - if (slave == null) { - throw new IllegalStateException(String.format("Slave machine '%s' not found into '%s'", vmName, prefix)); - } - - return slave; + throw new AzureCloudException(String.format("AzureCloud: createProvisionedSlave: Deployment failed, max tries reached for %s", deploymentName)); } @Override public Collection provision(final Label label, int workLoad) { LOGGER.log(Level.INFO, - "Azure Cloud: provision: start for label {0} workLoad {1}", new Object[] { label, workLoad }); + "AzureCloud: provision: start for label {0} workLoad {1}", new Object[] { label, workLoad }); final AzureSlaveTemplate template = getAzureSlaveTemplate(label); - // verify template - if (!verifyTemplate(template)) { - return Collections.emptyList(); - } - // round up the number of required machine int numberOfSlaves = (workLoad + template.getNoOfParallelJobs() - 1) / template.getNoOfParallelJobs(); final List plannedNodes = new ArrayList(numberOfSlaves); // reuse existing nodes if available + LOGGER.log(Level.INFO, "AzureCloud: provision: checking for node reuse options"); for (Computer slaveComputer : Jenkins.getInstance().getComputers()) { - LOGGER.log(Level.INFO, "Azure Cloud: provision: got slave computer {0}", slaveComputer.getName()); + if (numberOfSlaves == 0) { + break; + } if (slaveComputer instanceof AzureComputer && slaveComputer.isOffline()) { final AzureComputer azureComputer = AzureComputer.class.cast(slaveComputer); final AzureSlave slaveNode = azureComputer.getNode(); if (isNodeEligibleForReuse(slaveNode, template)) { - - LOGGER.log(Level.INFO, - "Azure Cloud: provision: \n - slave node {0}\n - slave template {1}", - new Object[] { slaveNode.getLabelString(), template.getLabels() }); - + LOGGER.log(Level.INFO, "AzureCloud: provision: slave computer eligible for reuse {0}", slaveComputer.getName()); try { if (AzureManagementServiceDelegate.virtualMachineExists(slaveNode)) { numberOfSlaves--; @@ -361,19 +460,21 @@ public class AzureCloud extends Cloud { Jenkins.getInstance().addNode(slaveNode); if (slaveNode.getSlaveLaunchMethod().equalsIgnoreCase("SSH")) { slaveNode.toComputer().connect(false).get(); - } else // Wait until node is online - { - waitUntilOnline(slaveNode); + } else { // Wait until node is online + waitUntilJNLPNodeIsOnline(slaveNode); } azureComputer.setAcceptingTasks(true); + slaveNode.clearCleanUpAction(); + slaveNode.setEligibleForReuse(false); return slaveNode; } }), template.getNoOfParallelJobs())); - } else { - slaveNode.setDeleteSlave(true); } } catch (Exception e) { - // ignore + // Couldn't bring the node back online. Mark it + // as needing deletion + azureComputer.setAcceptingTasks(false); + slaveNode.setCleanUpAction(CleanUpAction.DEFAULT, Messages._Shutdown_Slave_Failed_To_Revive()); } } } @@ -382,10 +483,30 @@ public class AzureCloud extends Cloud { // provision new nodes if required if (numberOfSlaves > 0) { try { - final String deployment = template.provisionSlaves( - new StreamTaskListener(System.out, Charset.defaultCharset()), numberOfSlaves); - - final int count = numberOfSlaves; + // Determine how many slaves we can actually provision from here and + // adjust our count (before deployment to avoid races) + int adjustedNumberOfSlaves = getAvailableVirtualMachineCount(numberOfSlaves); + if (adjustedNumberOfSlaves == 0) { + LOGGER.log(Level.INFO, "Not able to create any new nodes, at or above maximum VM count of {0}", + getMaxVirtualMachinesLimit()); + } + else if (adjustedNumberOfSlaves < numberOfSlaves) { + LOGGER.log(Level.INFO, "Able to create new nodes, but can only create {0} (desired {1})", + new Object[] { adjustedNumberOfSlaves, numberOfSlaves } ); + } + final int numberOfNewSlaves = adjustedNumberOfSlaves; + // Adjust number of nodes available by the number of created nodes. + // Negative to reduce number available. + this.adjustVirtualMachineCount(-adjustedNumberOfSlaves); + + ExecutorService executorService = Executors.newCachedThreadPool(); + Callable callableTask = new Callable() { + @Override + public AzureDeploymentInfo call() throws Exception { + return template.provisionSlaves(new StreamTaskListener(System.out, Charset.defaultCharset()), numberOfNewSlaves); + } + }; + final Future deploymentFuture = executorService.submit(callableTask); for (int i = 0; i < numberOfSlaves; i++) { final int index = i; @@ -395,35 +516,69 @@ public class AzureCloud extends Cloud { @Override public Node call() throws Exception { - final AzureSlave slave = provisionedSlave( + + // Wait for the future to complete + AzureDeploymentInfo info = deploymentFuture.get(); + + final String deploymentName = info.getDeploymentName(); + final String vmBaseName = info.getVmBaseName(); + final String vmName = String.format("%s%d", vmBaseName, index); + + AzureSlave slave = null; + try { + slave = createProvisionedSlave( template, - deployment, - index, - count, + vmName, + deploymentName, ServiceDelegateHelper.getConfiguration(template)); - - // Get virtual machine properties - LOGGER.log(Level.INFO, - "Azure Cloud: provision: Getting slave {0} ({1}) properties", - new Object[] { slave.getNodeName(), slave.getOsType() }); + } + catch (Exception e) { + LOGGER.log( + Level.SEVERE, + String.format("Failure creating provisioned slave '%s'", vmName), + e); + + // Attempt to terminate whatever was created + AzureManagementServiceDelegate.terminateVirtualMachine( + ServiceDelegateHelper.getConfiguration(template), vmName, + template.getResourceGroupName()); + template.getAzureCloud().adjustVirtualMachineCount(1); + // Update the template status given this new issue. + template.handleTemplateProvisioningFailure(e.getMessage(), FailureStage.PROVISIONING); + throw e; + } try { - template.setVirtualMachineDetails(slave); + LOGGER.log(Level.INFO, "Azure Cloud: provision: Adding slave {0} to Jenkins nodes", slave.getNodeName()); + // Place the node in blocked state while it starts. + slave.blockCleanUpAction(); + Jenkins.getInstance().addNode(slave); if (slave.getSlaveLaunchMethod().equalsIgnoreCase("SSH")) { - LOGGER.info("Azure Cloud: provision: Adding slave to azure nodes "); - Jenkins.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); - LOGGER.info("Azure Cloud: provision: Adding slave to azure nodes "); - Jenkins.getInstance().addNode(slave); // Wait until node is online - waitUntilOnline(slave); + waitUntilJNLPNodeIsOnline(slave); } + // Place node in default state, now can be + // dealt with by the cleanup task. + slave.clearCleanUpAction(); } catch (Exception e) { - template.handleTemplateStatus( - e.getMessage(), FailureStage.POSTPROVISIONING, slave); + LOGGER.log( + Level.SEVERE, + String.format("Failure to in post-provisioning for '%s'", vmName), + e); + + // Attempt to terminate whatever was created + AzureManagementServiceDelegate.terminateVirtualMachine( + ServiceDelegateHelper.getConfiguration(template), vmName, + template.getResourceGroupName()); + template.getAzureCloud().adjustVirtualMachineCount(1); + + // Update the template status + template.handleTemplateProvisioningFailure(vmName, FailureStage.POSTPROVISIONING); + + // Remove the node from jenkins + Jenkins.getInstance().removeNode(slave); throw e; } return slave; @@ -439,11 +594,17 @@ public class AzureCloud extends Cloud { } } + LOGGER.log(Level.INFO, + "AzureCloud: provision: asynchronous provision finished, returning {0} planned node(s)", plannedNodes.size()); return plannedNodes; } - /** this methods wait for node to be available */ - private void waitUntilOnline(final AzureSlave slave) { + /** + * Wait till a node that connects through JNLP comes online and connects to Jenkins. + * @param slave Node to wait for + * @throws Exception Throws if the wait time expires or other exception happens. + */ + private void waitUntilJNLPNodeIsOnline(final AzureSlave slave) throws Exception { LOGGER.log(Level.INFO, "Azure Cloud: waitUntilOnline: for slave {0}", slave.getDisplayName()); ExecutorService executorService = Executors.newCachedThreadPool(); Callable callableTask = new Callable() { @@ -465,8 +626,7 @@ public class AzureCloud extends Cloud { String result = future.get(30, TimeUnit.MINUTES); LOGGER.log(Level.INFO, "Azure Cloud: waitUntilOnline: node is alive , result {0}", result); } catch (Exception ex) { - LOGGER.log(Level.INFO, "Azure Cloud: waitUntilOnline: Failure waiting till online", ex); - markSlaveForDeletion(slave, Constants.JNLP_POST_PROV_LAUNCH_FAIL); + throw new AzureCloudException("Azure Cloud: waitUntilOnline: Failure waiting till online", ex); } finally { future.cancel(true); executorService.shutdown(); @@ -477,8 +637,7 @@ public class AzureCloud extends Cloud { * Checks if node configuration matches with template definition. */ private static boolean isNodeEligibleForReuse(AzureSlave slaveNode, AzureSlaveTemplate slaveTemplate) { - // Do not reuse slave if it is marked for deletion. - if (slaveNode.isDeleteSlave()) { + if (!slaveNode.isEligibleForReuse()) { return false; } @@ -495,14 +654,6 @@ public class AzureCloud extends Cloud { return false; } - private static void markSlaveForDeletion(AzureSlave slave, String message) { - slave.setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED, message); - if (slave.toComputer() != null) { - slave.toComputer().setTemporarilyOffline(true, OfflineCause.create(Messages._Slave_Failed_To_Connect())); - } - slave.setDeleteSlave(true); - } - @Extension public static class DescriptorImpl extends Descriptor { @@ -518,13 +669,18 @@ public class AzureCloud extends Cloud { public int getDefaultMaxVMLimit() { return Constants.DEFAULT_MAX_VM_LIMIT; } + + public String getDefaultResourceGroupName() { + return Constants.DEFAULT_RESOURCE_GROUP_NAME; + } public FormValidation doVerifyConfiguration( @QueryParameter String subscriptionId, @QueryParameter String clientId, @QueryParameter String clientSecret, @QueryParameter String oauth2TokenEndpoint, - @QueryParameter String serviceManagementURL) { + @QueryParameter String serviceManagementURL, + @QueryParameter String resourceGroupName) { if (StringUtils.isBlank(subscriptionId)) { return FormValidation.error("Error: Subscription ID is missing"); @@ -541,13 +697,17 @@ public class AzureCloud extends Cloud { if (StringUtils.isBlank(serviceManagementURL)) { serviceManagementURL = Constants.DEFAULT_MANAGEMENT_URL; } + if (StringUtils.isBlank(resourceGroupName)) { + resourceGroupName = Constants.DEFAULT_RESOURCE_GROUP_NAME; + } String response = AzureManagementServiceDelegate.verifyConfiguration( subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, - serviceManagementURL); + serviceManagementURL, + resourceGroupName); if (Constants.OP_SUCCESS.equalsIgnoreCase(response)) { return FormValidation.ok(Messages.Azure_Config_Success()); diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureCloudRetensionStrategy.java b/src/main/java/com/microsoftopentechnologies/azure/AzureCloudRetensionStrategy.java index a2be3e7..3c25a96 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureCloudRetensionStrategy.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureCloudRetensionStrategy.java @@ -21,6 +21,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; import com.microsoftopentechnologies.azure.retry.LinearRetryForAllExceptions; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import com.microsoftopentechnologies.azure.util.Constants; import com.microsoftopentechnologies.azure.util.ExecutionEngine; @@ -31,67 +32,85 @@ import java.util.logging.Level; public class AzureCloudRetensionStrategy extends RetentionStrategy { - public final long idleTerminationMillis; + // Configured idle termination + private final long idleTerminationMillis; private static final Logger LOGGER = Logger.getLogger(AzureManagementServiceDelegate.class.getName()); - + @DataBoundConstructor public AzureCloudRetensionStrategy(int idleTerminationMinutes) { this.idleTerminationMillis = TimeUnit2.MINUTES.toMillis(idleTerminationMinutes); } + /** + * Called by Jenkins to determine what to do with a particular node. + * Node could be shut down, deleted, etc. + * @param slaveNode Node to check + * @return Number of minutes before node will be checked again. + */ @Override public long check(final AzureComputer slaveNode) { - // if idleTerminationMinutes is zero then it means that never terminate the slave instance - // an active node or one that is not yet up and running are ignored as well - if (idleTerminationMillis > 0 && slaveNode.isIdle() && slaveNode.isProvisioned() - && idleTerminationMillis < (System.currentTimeMillis() - slaveNode.getIdleStartMilliseconds())) { - // block node for further tasks - slaveNode.setAcceptingTasks(false); - LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: check: Idle timeout reached for slave: {0}", - slaveNode.getName()); + // Determine whether we can recycle this machine. + // The CRS is the way that nodes that are currently operating "correctly" + // can be retained/reclaimed. Any failure modes need to be dealt with through + // the clean up task. + + boolean canRecycle = true; + // Node must be idle + canRecycle &= slaveNode.isIdle(); + // The node must also be online. This also implies not temporarily disconnected + // (like by a user). + canRecycle &= slaveNode.isOnline(); + // The configured idle time must be > 0 (which means leave forever) + canRecycle &= idleTerminationMillis > 0; + // The number of ms it's been idle must be greater than the current idle time. + canRecycle &= idleTerminationMillis < (System.currentTimeMillis() - slaveNode.getIdleStartMilliseconds()); + + if (slaveNode.getNode() == null) { + return 1; + } + + final AzureSlave slave = slaveNode.getNode(); + + if (canRecycle) { + LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: check: Idle timeout reached for slave: {0}, action: {1}", + new Object [] {slaveNode.getName(), slave.isShutdownOnIdle() ? "shutdown" : "delete"} ); java.util.concurrent.Callable task = new java.util.concurrent.Callable() { - @Override public Void call() throws Exception { - LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: going to idleTimeout slave: {0}", + // Block cleanup while we execute so the cleanup task doesn't try to take it + // away (node will go offline). Also blocks cleanup in case of shutdown. + slave.blockCleanUpAction(); + if (slave.isShutdownOnIdle()) { + LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: going to idleTimeout slave: {0}", slaveNode.getName()); - slaveNode.getNode().idleTimeout(); + slave.shutdown(Messages._Idle_Timeout_Shutdown()); + } else { + slave.deprovision(Messages._Idle_Timeout_Delete()); + } return null; } }; try { - ExecutionEngine.executeWithRetry(task, + ExecutionEngine.executeAsync(task, new LinearRetryForAllExceptions( 30, // maxRetries 30, // waitinterval 30 * 60 // timeout )); } catch (AzureCloudException ae) { - LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: check: could not terminate or shutdown {0}", - slaveNode.getName()); + LOGGER.log(Level.INFO, "AzureCloudRetensionStrategy: check: could not terminate or shutdown {0}: {1}", + new Object [] { slaveNode.getName(), ae }); + // If we have an exception, set the slave for deletion. It's unlikely we'll be able to shut it down properly ever. + slaveNode.getNode().setCleanUpAction(CleanUpAction.DELETE, Messages._Failed_Initial_Shutdown_Or_Delete()); } catch (Exception e) { LOGGER.log(Level.INFO, - "AzureCloudRetensionStrategy: execute: Exception occured while calling timeout on node", e); - // We won't get exception for RNF , so for other exception types we can retry - if (e.getMessage().contains("not found in the currently deployed service")) { - LOGGER.info("AzureCloudRetensionStrategy: execute: Slave does not exist " - + "in the subscription anymore, setting shutdownOnIdle to True"); - slaveNode.getNode().setShutdownOnIdle(true); - } - } - // close channel - try { - slaveNode.setProvisioned(false); - if (slaveNode.getChannel() != null) { - slaveNode.getChannel().close(); - } - } catch (Exception e) { - LOGGER.log(Level.INFO, - "AzureCloudRetensionStrategy: check: exception occured while closing channel for: {0}", - slaveNode.getName()); + "AzureCloudRetensionStrategy: check: Exception occured while calling timeout on node {0}: {1}", + new Object [] { slaveNode.getName(), e }); + // If we have an exception, set the slave for deletion. It's unlikely we'll be able to shut it down properly ever. + slaveNode.getNode().setCleanUpAction(CleanUpAction.DELETE, Messages._Failed_Initial_Shutdown_Or_Delete()); } } return 1; diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureComputer.java b/src/main/java/com/microsoftopentechnologies/azure/AzureComputer.java index 83d6938..a39f769 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureComputer.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureComputer.java @@ -15,6 +15,10 @@ */ package com.microsoftopentechnologies.azure; +import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; +import com.microsoftopentechnologies.azure.retry.NoRetryStrategy; +import com.microsoftopentechnologies.azure.util.CleanUpAction; +import com.microsoftopentechnologies.azure.util.ExecutionEngine; import java.io.IOException; import java.util.logging.Logger; @@ -23,13 +27,14 @@ import org.kohsuke.stapler.HttpResponse; import hudson.slaves.AbstractCloudComputer; import hudson.slaves.OfflineCause; +import java.util.concurrent.Callable; import java.util.logging.Level; public class AzureComputer extends AbstractCloudComputer { private static final Logger LOGGER = Logger.getLogger(AzureComputer.class.getName()); - private boolean provisioned = false; + private boolean setOfflineByUser = false; public AzureComputer(final AzureSlave slave) { super(slave); @@ -38,56 +43,80 @@ public class AzureComputer extends AbstractCloudComputer { @Override public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); - AzureSlave slave = getNode(); - + this.setAcceptingTasks(false); + final AzureSlave slave = getNode(); + if (slave != null) { - LOGGER.log(Level.INFO, "AzureComputer: doDoDelete called for slave {0}", slave.getNodeName()); - setTemporarilyOffline(true, OfflineCause.create(Messages._Delete_Slave())); - slave.setDeleteSlave(true); + Callable task = new Callable() { + @Override + public Void call() throws Exception { + LOGGER.log(Level.INFO, "AzureComputer: doDoDelete called for slave {0}", slave.getNodeName()); + try { + // Deprovision + slave.deprovision(Messages._User_Delete()); + } catch (Exception e) { + LOGGER.log(Level.INFO, "AzureComputer: doDoDelete: Exception occurred while deleting slave", e); + throw new AzureCloudException("AzureComputer: doDoDelete: Exception occurred while deleting slave", e); + } + return null; + } + }; try { - deleteSlave(); - } catch (Exception e) { - LOGGER.log(Level.INFO, "AzureComputer: doDoDelete: Exception occurred while deleting slave", e); - - throw new IOException( - "Error deleting node, jenkins will try to clean up node automatically after some time. ", e); + ExecutionEngine.executeAsync(task, new NoRetryStrategy()); + } catch (AzureCloudException exception) { + // No need to throw exception back, just log and move on. + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: failed to shutdown/delete " + slave.getDisplayName(), + exception); } } + return new HttpRedirect(".."); } - public void deleteSlave() throws Exception, InterruptedException { - LOGGER.log(Level.INFO, "AzureComputer : deleteSlave: Deleting {0} slave", getName()); - - AzureSlave slave = getNode(); - - if (slave != null) { - if (slave.getChannel() != null) { - slave.getChannel().close(); - } - - try { - slave.deprovision(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "AzureComputer : Exception occurred while deleting {0} slave", getName()); - LOGGER.log(Level.SEVERE, "Root cause", e); - throw e; - } - } + public boolean isSetOfflineByUser() { + return setOfflineByUser; } - public void setProvisioned(boolean provisioned) { - this.provisioned = provisioned; + public void setSetOfflineByUser(boolean setOfflineByUser) { + this.setOfflineByUser = setOfflineByUser; } - - public boolean isProvisioned() { - return this.provisioned; - } - + + /** + * Wait until the node is online + * @throws InterruptedException + */ @Override public void waitUntilOnline() throws InterruptedException { super.waitUntilOnline(); - setProvisioned(true); + } + + /** + * We use temporary offline settings to do investigation of machines. + * To avoid deletion, we assume this came through a user call and set a bit. Where + * this plugin might set things temp-offline (vs. disconnect), we'll reset the bit + * after calling setTemporarilyOffline + * @param setOffline + * @param oc + */ + @Override + public void setTemporarilyOffline(boolean setOffline, OfflineCause oc) { + setSetOfflineByUser(setOffline); + super.setTemporarilyOffline(setOffline, oc); + } + + /** + * We use temporary offline settings to do investigation of machines. + * To avoid deletion, we assume this came through a user call and set a bit. Where + * this plugin might set things temp-offline (vs. disconnect), we'll reset the bit + * after calling setTemporarilyOffline + * @param setOffline + * @param oc + */ + @Override + public void setTemporarilyOffline(boolean setOffline) { + setSetOfflineByUser(setOffline); + super.setTemporarilyOffline(setOffline); } } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureDeploymentInfo.java b/src/main/java/com/microsoftopentechnologies/azure/AzureDeploymentInfo.java new file mode 100644 index 0000000..16fecc9 --- /dev/null +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureDeploymentInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 mmitche. + * + * 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; + +/** + * Simple class with info from a new Azure deployment + * @author mmitche + */ +public class AzureDeploymentInfo { + private String deploymentName; + private String vmBaseName; + private int vmCount; + + public AzureDeploymentInfo(String deploymentName, String vmBaseName, int vmCount) { + this.deploymentName = deploymentName; + this.vmBaseName = vmBaseName; + this.vmCount = vmCount; + } + + public String getDeploymentName() { + return deploymentName; + } + + public String getVmBaseName() { + return vmBaseName; + } + + public int getVmCount() { + return vmCount; + } + + +} diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureManagementServiceDelegate.java b/src/main/java/com/microsoftopentechnologies/azure/AzureManagementServiceDelegate.java index 2956506..9b9b3d7 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureManagementServiceDelegate.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureManagementServiceDelegate.java @@ -18,14 +18,19 @@ package com.microsoftopentechnologies.azure; import com.fasterxml.jackson.databind.JsonNode; import hudson.model.Descriptor.FormException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.microsoft.azure.management.compute.ComputeManagementClient; import com.microsoft.azure.management.compute.VirtualMachineImageOperations; +import com.microsoft.azure.management.compute.VirtualMachineOperations; import com.microsoft.azure.management.compute.models.VirtualMachineGetResponse; import com.microsoft.azure.management.compute.models.InstanceViewStatus; import com.microsoft.azure.management.compute.models.ListParameters; +import com.microsoft.azure.management.compute.models.StorageProfile; +import com.microsoft.azure.management.compute.models.VirtualMachine; import com.microsoft.azure.management.compute.models.VirtualMachineImageGetParameters; +import com.microsoft.azure.management.compute.models.VirtualMachineListResponse; import com.microsoft.azure.management.network.NetworkResourceProviderClient; import com.microsoft.azure.management.network.NetworkResourceProviderService; import com.microsoft.azure.management.network.models.NetworkInterface; @@ -43,10 +48,12 @@ import com.microsoft.azure.management.resources.models.ResourceGroup; import com.microsoft.azure.management.storage.StorageManagementClient; import com.microsoft.azure.management.storage.StorageManagementService; import com.microsoft.azure.management.storage.models.StorageAccount; +import com.microsoft.azure.management.storage.models.StorageAccountKeys; import com.microsoft.azure.management.storage.models.StorageAccountListResponse; +import com.microsoft.azure.storage.blob.CloudBlobContainer; +import com.microsoft.azure.storage.core.PathUtility; import java.io.IOException; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.HashMap; @@ -60,30 +67,25 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; -import javax.xml.parsers.ParserConfigurationException; -import org.codehaus.jackson.JsonFactory; -import org.codehaus.jackson.JsonGenerator; -import org.xml.sax.SAXException; import com.microsoft.windowsazure.Configuration; import com.microsoft.windowsazure.exception.ServiceException; -import com.microsoft.windowsazure.management.ManagementClient; -import com.microsoft.windowsazure.management.models.LocationsListResponse; -import com.microsoft.windowsazure.management.models.LocationsListResponse.Location; -import com.microsoft.windowsazure.management.models.RoleSizeListResponse; -import com.microsoft.windowsazure.management.models.RoleSizeListResponse.RoleSize; import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; import com.microsoftopentechnologies.azure.exceptions.UnrecoverableCloudException; import com.microsoftopentechnologies.azure.retry.ExponentialRetryStrategy; import com.microsoftopentechnologies.azure.retry.NoRetryStrategy; import com.microsoftopentechnologies.azure.util.AzureUtil; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import com.microsoftopentechnologies.azure.util.Constants; import com.microsoftopentechnologies.azure.util.ExecutionEngine; import com.microsoftopentechnologies.azure.util.FailureStage; import java.io.InputStream; +import java.net.URI; import java.util.Arrays; import java.util.logging.Level; +import jenkins.model.Jenkins; +import jenkins.slaves.JnlpSlaveAgentProtocol; import org.apache.commons.lang.StringUtils; @@ -97,50 +99,85 @@ public class AzureManagementServiceDelegate { private static final Logger LOGGER = Logger.getLogger(AzureManagementServiceDelegate.class.getName()); - private static final String EMBEDDED_TEMPLATE_FILENAME = "/templateValue.json"; + private static final String EMBEDDED_TEMPLATE_FILENAME = "/referenceImageTemplate.json"; + + private static final String EMBEDDED_TEMPLATE_WITH_SCRIPT_FILENAME = "/referenceImageTemplateWithScript.json"; - private static final String EMBEDDED_TEMPLATE_IMAGE_FILENAME = "/templateImageValue.json"; + private static final String EMBEDDED_TEMPLATE_IMAGE_FILENAME = "/customImageTemplate.json"; + + private static final String EMBEDDED_TEMPLATE_IMAGE_WITH_SCRIPT_FILENAME = "/customImageTemplateWithScript.json"; private static final String IMAGE_CUSTOM_REFERENCE = "custom"; private static final Map> AVAILABLE_ROLE_SIZES = getAvailableRoleSizes(); - private static final List AVAILABLE_LOCATIONS_STD = getAvailableLocations(); + private static final Map AVAILABLE_LOCATIONS_STD = getAvailableLocationsStandard(); - private static final List AVAILABLE_LOCATIONS_CHINA = getAvailableLocationsChina(); + private static final Map AVAILABLE_LOCATIONS_CHINA = getAvailableLocationsChina(); - public static String deployment(final AzureSlaveTemplate template, final int numberOfslaves) + private static final Map AVAILABLE_LOCATIONS_ALL = getAvailableLocationsAll(); + + /** + * Creates a new deployment of VMs based on the provided template + * @param template Template to deploy + * @param numberOfSlaves Number of slaves to create + * @return The base name for the VMs that were created + * @throws AzureCloudException + */ + public static AzureDeploymentInfo createDeployment(final AzureSlaveTemplate template, final int numberOfSlaves) throws AzureCloudException { try { LOGGER.log(Level.INFO, - "AzureManagementServiceDelegate: deployment: Initializing deployment for slaveTemaple {0}", + "AzureManagementServiceDelegate: createDeployment: Initializing deployment for slaveTemplate {0}", template.getTemplateName()); - final ResourceManagementClient client = ServiceDelegateHelper.getResourceManagementClient( - ServiceDelegateHelper.getConfiguration(template)); - - final long ts = System.currentTimeMillis(); + Configuration config = ServiceDelegateHelper.getConfiguration(template); + final ResourceManagementClient client = ServiceDelegateHelper.getResourceManagementClient(config); + final String deploymentName = AzureUtil.getDeploymentName(template.getTemplateName()); + final String vmBaseName = AzureUtil.getVMBaseName(template.getTemplateName(), template.getOsType(), numberOfSlaves); + final String locationName = getLocationName(template.getLocation()); + LOGGER.log(Level.INFO, + "AzureManagementServiceDelegate: createDeployment: Creating a new deployment {0} with VM base name {1}", + new Object[] { deploymentName, vmBaseName} ); + + client.getResourceGroupsOperations().createOrUpdate( - Constants.RESOURCE_GROUP_NAME, - new ResourceGroup(template.getLocation())); + template.getResourceGroupName(), + new ResourceGroup(locationName)); final Deployment deployment = new Deployment(); final DeploymentProperties properties = new DeploymentProperties(); deployment.setProperties(properties); final InputStream embeddedTemplate; + final boolean useCustomScriptExtension = + template.getOsType().equals(Constants.OS_TYPE_WINDOWS) && !StringUtils.isBlank(template.getInitScript()) && + template.getSlaveLaunchMethod().equals(Constants.LAUNCH_METHOD_JNLP); // check if a custom image id has been provided otherwise work with publisher and offer - if (template.getImageReferenceType().equals(IMAGE_CUSTOM_REFERENCE) - && StringUtils.isNotBlank(template.getImage())) { - LOGGER.log(Level.INFO, "Use embedded deployment template {0}", EMBEDDED_TEMPLATE_IMAGE_FILENAME); - embeddedTemplate + if (template.getImageReferenceType().equals(IMAGE_CUSTOM_REFERENCE)) { + if (useCustomScriptExtension) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: createDeployment: Use embedded deployment template {0}", EMBEDDED_TEMPLATE_IMAGE_WITH_SCRIPT_FILENAME); + embeddedTemplate + = AzureManagementServiceDelegate.class.getResourceAsStream(EMBEDDED_TEMPLATE_IMAGE_WITH_SCRIPT_FILENAME); + } + else { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: createDeployment: Use embedded deployment template (with script) {0}", EMBEDDED_TEMPLATE_IMAGE_FILENAME); + embeddedTemplate = AzureManagementServiceDelegate.class.getResourceAsStream(EMBEDDED_TEMPLATE_IMAGE_FILENAME); + } } else { - LOGGER.log(Level.INFO, "Use embedded deployment template {0}", EMBEDDED_TEMPLATE_FILENAME); - embeddedTemplate + if (useCustomScriptExtension) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: createDeployment: Use embedded deployment template (with script) {0}", EMBEDDED_TEMPLATE_WITH_SCRIPT_FILENAME); + embeddedTemplate + = AzureManagementServiceDelegate.class.getResourceAsStream(EMBEDDED_TEMPLATE_WITH_SCRIPT_FILENAME); + } + else { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: createDeployment: Use embedded deployment template {0}", EMBEDDED_TEMPLATE_FILENAME); + embeddedTemplate = AzureManagementServiceDelegate.class.getResourceAsStream(EMBEDDED_TEMPLATE_FILENAME); + } } final ObjectMapper mapper = new ObjectMapper(); @@ -149,17 +186,11 @@ public class AzureManagementServiceDelegate { // Add count variable for loop.... final ObjectNode count = mapper.createObjectNode(); count.put("type", "int"); - count.put("defaultValue", numberOfslaves); + count.put("defaultValue", numberOfSlaves); ObjectNode.class.cast(tmp.get("parameters")).replace("count", count); - - if (StringUtils.isBlank(template.getTemplateName())) { - throw new AzureCloudException( - String.format("Invalid template name '%s'", template.getTemplateName())); - } - - final String name = String.format("%s%d", template.getTemplateName(), ts); - ObjectNode.class.cast(tmp.get("variables")).put("vmName", name); - ObjectNode.class.cast(tmp.get("variables")).put("location", template.getLocation()); + + ObjectNode.class.cast(tmp.get("variables")).put("vmName", vmBaseName); + ObjectNode.class.cast(tmp.get("variables")).put("location", locationName); if (StringUtils.isNotBlank(template.getImagePublisher())) { ObjectNode.class.cast(tmp.get("variables")).put("imagePublisher", template.getImagePublisher()); @@ -180,6 +211,34 @@ public class AzureManagementServiceDelegate { if (StringUtils.isNotBlank(template.getImage())) { ObjectNode.class.cast(tmp.get("variables")).put("image", template.getImage()); } + + // If using the custom script extension (vs. SSH) to startup the powershell scripts, + // add variables for that and upload the init script to the storage account + if (useCustomScriptExtension) { + ObjectNode.class.cast(tmp.get("variables")).put("jenkinsServerURL", Jenkins.getInstance().getRootUrl()); + // Calculate the client secrets. The secrets are based off the machine name, + ArrayNode clientSecretsNode = ObjectNode.class.cast(tmp.get("variables")).putArray("clientSecrets"); + for (int i = 0; i < numberOfSlaves; i++) { + clientSecretsNode.add( + JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(String.format("%s%d", vmBaseName, i))); + } + // Upload the startup script to blob storage + String scriptName = String.format("%s%s", deploymentName, "init.ps1"); + String scriptUri = uploadCustomScript(template, scriptName); + ObjectNode.class.cast(tmp.get("variables")).put("startupScriptURI", scriptUri); + ObjectNode.class.cast(tmp.get("variables")).put("startupScriptName", scriptName); + + String storageAccountKey = ServiceDelegateHelper.getStorageManagementClient(config).getStorageAccountsOperations().listKeys( + template.getResourceGroupName(), template.getStorageAccountName()) + .getStorageAccountKeys().getKey1(); + + final ObjectNode storageAccountKeyNode = mapper.createObjectNode(); + storageAccountKeyNode.put("type", "secureString"); + storageAccountKeyNode.put("defaultValue", storageAccountKey); + + // Add the storage account key + ObjectNode.class.cast(tmp.get("parameters")).replace("storageAccountKey", storageAccountKeyNode); + } ObjectNode.class.cast(tmp.get("variables")).put("vmSize", template.getVirtualMachineSize()); ObjectNode.class.cast(tmp.get("variables")).put("adminUsername", template.getAdminUserName()); @@ -201,131 +260,38 @@ public class AzureManagementServiceDelegate { properties.setMode(DeploymentMode.Incremental); properties.setTemplate(tmp.toString()); - final String deploymentName = String.valueOf(ts); - client.getDeploymentsOperations().createOrUpdate(Constants.RESOURCE_GROUP_NAME, deploymentName, deployment); - return deploymentName; + client.getDeploymentsOperations().createOrUpdate(template.getResourceGroupName(), deploymentName, deployment); + + return new AzureDeploymentInfo(deploymentName, vmBaseName, numberOfSlaves); } catch (Exception e) { LOGGER.log(Level.SEVERE, "AzureManagementServiceDelegate: deployment: Unable to deploy", e); + // Pass the info off to the template so that it can be queued for update. + template.handleTemplateProvisioningFailure(e.getMessage(), FailureStage.PROVISIONING); throw new AzureCloudException(e); } } - + /** - * Handle provisioning errors. - * - * @param ex - * @param template - * @param deploymentName - * @throws AzureCloudException + * Uploads the custom script for a template to blob storage + * @param template Template containing script to upload + * @return URI of script */ - public static void handleProvisioningException( - final Exception ex, final AzureSlaveTemplate template, final String deploymentName) - throws AzureCloudException { - // conflict error - wait for 1 minute and try again - if (AzureUtil.isConflictError(ex.getMessage())) { - if (AzureUtil.isDeploymentAlreadyOccupied(ex.getMessage())) { - LOGGER.info("AzureManagementServiceDelegate: handleProvisioningServiceException: " - + "Deployment already occupied"); - - // Throw exception so that in retry this will go through - throw new AzureCloudException( - "Provisioning Failure: Exception occured while creating virtual machine. Root cause: " + ex. - getMessage()); - } else { - LOGGER.info("AzureManagementServiceDelegate: handleProvisioningServiceException: " - + "conflict error: waiting for a minute and will try again"); - try { - Thread.sleep(60 * 1000); - } catch (InterruptedException e) { - //ignore - } - } - } else if (AzureUtil.isBadRequestOrForbidden(ex.getMessage())) { - LOGGER.info( - "AzureManagementServiceDelegate: handleProvisioningServiceException: Got bad request or forbidden"); - // no point in retrying - template.handleTemplateStatus( - "Provisioning Failure: Exception occured while creating virtual machine. Root cause: " + ex. - getMessage(), FailureStage.PROVISIONING, null); - throw new AzureCloudException( - "Provisioning Failure: Exception occured while creating virtual machine. Root cause: " + ex. - getMessage()); - } else if (AzureUtil.isDeploymentNotFound(ex.getMessage(), deploymentName)) { - LOGGER.info("AzureManagementServiceDelegate: handleProvisioningServiceException: Deployment not found"); - - // Throw exception so that in retry this will go through - throw new AzureCloudException( - "Provisioning Failure: Exception occured while creating virtual machine. Root cause: " + ex. - getMessage()); - } else { - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: handleProvisioningException: {0}", ex); - // set template status to disabled so that jenkins won't provision more slaves - template.handleTemplateStatus( - "Provisioning Failure: Exception occured while creating virtual machine. Root cause: " + ex. - getMessage(), FailureStage.PROVISIONING, null); - // wait for 10 seconds and then retry - try { - Thread.sleep(10 * 1000); - } catch (InterruptedException e) { - //ignore - } - } - } - - /** - * JSON string custom script public config value - * - * @param sasURL - * @param fileName - * @param jenkinsServerURL - * @param vmName - * @param jnlpSecret - * @return - * @throws Exception - */ - public static String getCustomScriptPublicConfigValue( - final String sasURL, - final String fileName, - final String jenkinsServerURL, - final String vmName, - final String jnlpSecret) throws Exception { - JsonFactory factory = new JsonFactory(); - StringWriter stringWriter = new StringWriter(); - JsonGenerator json = factory.createJsonGenerator(stringWriter); - - json.writeStartObject(); - json.writeArrayFieldStart("fileUris"); - json.writeString(sasURL); - json.writeEndArray(); - json.writeStringField("commandToExecute", "powershell -ExecutionPolicy Unrestricted -file " + fileName - + " " + jenkinsServerURL + " " + vmName + " " + jnlpSecret + " " + " 2>>c:\\error.log"); - json.writeEndObject(); - json.close(); - return stringWriter.toString(); - } - - /** - * JSON string for custom script private config value. - * - * @param storageAccountName - * @param storageAccountKey - * @return - * @throws Exception - */ - public static String getCustomScriptPrivateConfigValue( - final String storageAccountName, - final String storageAccountKey) - throws Exception { - JsonFactory factory = new JsonFactory(); - StringWriter stringWriter = new StringWriter(); - JsonGenerator json = factory.createJsonGenerator(stringWriter); - - json.writeStartObject(); - json.writeStringField("storageAccountName", storageAccountName); - json.writeStringField("storageAccountKey", storageAccountKey); - json.writeEndObject(); - json.close(); - return stringWriter.toString(); + private static String uploadCustomScript(final AzureSlaveTemplate template, final String targetScriptName) throws Exception { + Configuration config = ServiceDelegateHelper.getConfiguration(template); + StorageManagementClient client = ServiceDelegateHelper.getStorageManagementClient(config); + + // Get the storage account name and key + String targetStorageAccount = template.getStorageAccountName(); + String resourceGroupName = template.getResourceGroupName(); + String storageAccountKey = client.getStorageAccountsOperations().listKeys(resourceGroupName, targetStorageAccount) + .getStorageAccountKeys().getKey1(); + String scriptText = template.getInitScript(); + + String blobURL = StorageServiceDelegate.uploadFileToStorage( + config, targetStorageAccount, storageAccountKey, + client.getBaseUri().toString(), resourceGroupName, Constants.CONFIG_CONTAINER_NAME, + targetScriptName, scriptText.getBytes("UTF-8")); + return blobURL; } /** @@ -342,14 +308,14 @@ public class AzureManagementServiceDelegate { ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(config); final VirtualMachineGetResponse vm - = client.getVirtualMachinesOperations().get(Constants.RESOURCE_GROUP_NAME, azureSlave.getNodeName()); + = client.getVirtualMachinesOperations().get(template.getResourceGroupName(), azureSlave.getNodeName()); final String ipRef = vm.getVirtualMachine().getNetworkProfile().getNetworkInterfaces().get(0). getReferenceUri(); final NetworkInterface netIF = NetworkResourceProviderService.create(config). getNetworkInterfacesOperations().get( - Constants.RESOURCE_GROUP_NAME, + template.getResourceGroupName(), ipRef.substring(ipRef.lastIndexOf("/") + 1, ipRef.length())). getNetworkInterface(); @@ -357,7 +323,7 @@ public class AzureManagementServiceDelegate { final PublicIpAddress pubIP = NetworkResourceProviderService.create(config). getPublicIpAddressesOperations().get( - Constants.RESOURCE_GROUP_NAME, + template.getResourceGroupName(), nicRef.substring(nicRef.lastIndexOf("/") + 1, nicRef.length())). getPublicIpAddress(); @@ -365,38 +331,56 @@ public class AzureManagementServiceDelegate { azureSlave.setPublicDNSName(pubIP.getDnsSettings().getFqdn()); azureSlave.setSshPort(Constants.DEFAULT_SSH_PORT); - LOGGER.log(Level.INFO, "Azure slave details: {0}", azureSlave); + LOGGER.log(Level.INFO, "Azure slave details:\nnodeName{0}\nadminUserName={1}\nshutdownOnIdle={2}\nretentionTimeInMin={3}\nlabels={4}", + new Object[] { azureSlave.getNodeName(), azureSlave.getAdminUserName(), azureSlave.isShutdownOnIdle(), + azureSlave.getRetentionTimeInMin(), azureSlave.getLabelString()}); } - - public static boolean virtualMachineExists(final AzureSlave slave) { - final String name = slave.getNodeName(); - - LOGGER.log(Level.INFO, "{0}: virtualMachineExists: check for {1}", - new Object[] { AzureManagementServiceDelegate.class.getSimpleName(), name }); + + /** + * Determines whether a virtual machine exists. + * @param configuration Configuration for the subscription + * @param vmName Name of the VM. + * @param resourceGroupName Resource group of the VM. + * @return + */ + private static boolean virtualMachineExists(final Configuration config, final String vmName, final String resourceGroupName) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: check for {0}", vmName); try { - final ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(slave); - client.getVirtualMachinesOperations().get(Constants.RESOURCE_GROUP_NAME, name); - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} exists", name); + final ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(config); + client.getVirtualMachinesOperations().get(resourceGroupName, vmName); + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} exists", vmName); return true; } catch (ServiceException se) { if (Constants.ERROR_CODE_RESOURCE_NF.equalsIgnoreCase(se.getError().getCode())) { - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} doesn't exist", name); - return false; - } - } catch (UnrecoverableCloudException uce) { - if (uce.getCause() instanceof UnrecoverableCloudException) { - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: unrecoverable VM", uce); + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} doesn't exist", vmName); return false; } } catch (Exception e) { //For rest of the errors just assume vm exists } - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} may exist", name); + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: virtualMachineExists: {0} may exist", vmName); return true; } + /** + * Determines whether a given slave exists. + * @param slave Slave to check + * @return True if the slave exists, false otherwise + */ + public static boolean virtualMachineExists(final AzureSlave slave) { + try { + Configuration config = ServiceDelegateHelper.getConfiguration(slave); + return virtualMachineExists(config, slave.getNodeName(), slave.getResourceGroupName()); + } + catch (Exception e) { + LOGGER.log(Level.INFO, + "AzureManagementServiceDelegate: virtualMachineExists: error while determining whether vm exists", e); + return false; + } + } + /** * Creates Azure slave object with necessary info. * @@ -438,6 +422,7 @@ public class AzureManagementServiceDelegate { template.getAdminPassword(), template.getJvmOptions(), template.isShutdownOnIdle(), + false, deploymentName, template.getRetentionTimeInMin(), template.getInitScript(), @@ -447,7 +432,11 @@ public class AzureManagementServiceDelegate { azureCloud.getOauth2TokenEndpoint(), azureCloud.getServiceManagementURL(), template.getSlaveLaunchMethod(), - false); + CleanUpAction.DEFAULT, + null, + template.getResourceGroupName(), + template.getExecuteInitScriptAsRoot(), + template.getDoNotUseMachineIfInitFails()); } catch (FormException e) { throw new AzureCloudException("AzureManagementServiceDelegate: parseResponse: " + "Exception occured while creating slave object", e); @@ -468,12 +457,44 @@ public class AzureManagementServiceDelegate { return storageAccounts; } - private static List getAvailableLocations() { - return Arrays.asList(new String[] { "East US","West US","South Central US","Central US","North Central US","East US 2","North Europe","West Europe","Southeast Asia","East Asia","Japan West","Japan East","Brazil South","Australia Southeast","Australia East","Central India","South India","West India" }); + /** + * Gets a map of available locations mapping display name -> name (usable in template) + * @return + */ + private static Map getAvailableLocationsStandard() { + final Map locations = new HashMap(); + locations.put("East US", "eastus"); + locations.put("West US", "westus"); + locations.put("South Central US", "southcentralus"); + locations.put("Central US", "centralus"); + locations.put("North Central US", "northcentralus"); + locations.put("North Europe", "northeurope"); + locations.put("West Europe", "westeurope"); + locations.put("Southeast Asia", "southeastasia"); + locations.put("East Asia", "eastasia"); + locations.put("Japan West", "japanwest"); + locations.put("Japan East", "japaneast"); + locations.put("Brazil South", "brazilsouth"); + locations.put("Australia Southeast", "australiasoutheast"); + locations.put("Australia East", "australiaeast"); + locations.put("Central India", "centralindia"); + locations.put("South India", "southindia"); + locations.put("West India", "westindia"); + return locations; } - private static List getAvailableLocationsChina() { - return Arrays.asList(new String[] { "China North","China East" }); + private static Map getAvailableLocationsChina() { + final Map locations = new HashMap(); + locations.put("China North", "chinanorth"); + locations.put("China East", "chinaeast"); + return locations; + } + + private static Map getAvailableLocationsAll() { + final Map locations = new HashMap(); + locations.putAll(getAvailableLocationsStandard()); + locations.putAll(getAvailableLocationsChina()); + return locations; } /** @@ -511,11 +532,11 @@ public class AzureManagementServiceDelegate { } /** - * Gets list of Azure datacenter locations which supports Persistent VM role. + * Gets map of Azure datacenter locations which supports Persistent VM role. * Today this is hardcoded pulling from the array, because the old form of * certificate based auth appears to be required. */ - public static List getVirtualMachineLocations(String serviceManagementUrl) { + public static Map getVirtualMachineLocations(String serviceManagementUrl) { if (serviceManagementUrl != null && serviceManagementUrl.toLowerCase().contains("china")) { return AVAILABLE_LOCATIONS_CHINA; } @@ -540,6 +561,7 @@ public class AzureManagementServiceDelegate { * @param oauth2TokenEndpoint * @param clientSecret * @param serviceManagementURL + * @param resourceGroupName * @return */ public static String verifyConfiguration( @@ -547,27 +569,41 @@ public class AzureManagementServiceDelegate { final String clientId, final String clientSecret, final String oauth2TokenEndpoint, - final String serviceManagementURL) { - try { - return verifyConfiguration(ServiceDelegateHelper.loadConfiguration( - subscriptionId, - clientId, - clientSecret, - oauth2TokenEndpoint, - serviceManagementURL)); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error validating configuration", e); - return "Failure: Exception occured while validating subscription configuration " + e; + final String serviceManagementURL, + final String resourceGroupName) { + if (StringUtils.isBlank(subscriptionId) + || StringUtils.isBlank(clientId) + || StringUtils.isBlank(oauth2TokenEndpoint) + || StringUtils.isBlank(clientSecret) + || StringUtils.isBlank(resourceGroupName)) { + + return Messages.Azure_GC_Template_Val_Profile_Missing(); + } else { + try { + // Load up the configuration now and do a live verification + Configuration config = ServiceDelegateHelper.loadConfiguration( + subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL); + + if (!verifyConfiguration(config, resourceGroupName).equals(Constants.OP_SUCCESS)) { + return Messages.Azure_GC_Template_Val_Profile_Err(); + } + } + catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error validating profile", e); + return Messages.Azure_GC_Template_Val_Profile_Err(); + } } + return Constants.OP_SUCCESS; } - public static String verifyConfiguration(final Configuration config) { + public static String verifyConfiguration(final Configuration config, final String resourceGroupName) { + Callable task = new Callable() { @Override public String call() throws Exception { ServiceDelegateHelper.getStorageManagementClient(config).getStorageAccountsOperations(). - checkNameAvailability("CI_SYSTEM"); + checkNameAvailability("CI_SYSTEM"); return Constants.OP_SUCCESS; } }; @@ -592,13 +628,13 @@ public class AzureManagementServiceDelegate { * @return * @throws Exception */ - public static String getVirtualMachineStatus(final Configuration config, final String vmName) + public static String getVirtualMachineStatus(final Configuration config, final String vmName, final String resourceGroupName) throws Exception { String powerstatus = StringUtils.EMPTY; String provisioning = StringUtils.EMPTY; final VirtualMachineGetResponse vm = ServiceDelegateHelper.getComputeManagementClient(config). - getVirtualMachinesOperations().getWithInstanceView(Constants.RESOURCE_GROUP_NAME, vmName); + getVirtualMachinesOperations().getWithInstanceView(resourceGroupName, vmName); for (InstanceViewStatus instanceStatus : vm.getVirtualMachine().getInstanceView().getStatuses()) { if (instanceStatus.getCode().startsWith("ProvisioningState/")) { @@ -609,15 +645,15 @@ public class AzureManagementServiceDelegate { } } - LOGGER.log(Level.INFO, "Statuses:\n\tPowerState: {0}\n\tProvisioning: {1}", + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: getVirtualMachineStatus:\n\tPowerState: {0}\n\tProvisioning: {1}", new Object[] { powerstatus, provisioning }); return "succeeded".equalsIgnoreCase(provisioning) - ? powerstatus.toUpperCase() : Constants.STOPPED_DEALLOCATED_VM_STATUS; + ? powerstatus.toUpperCase() : Constants.PROVISIONING_OR_DEPROVISIONING_VM_STATUS; } /** - * Checks if VM is reachable and in a valid state to connect. + * Checks if VM is reachable and in a valid state to connect (or getting ready to do so). * * @param slave * @return @@ -625,27 +661,30 @@ public class AzureManagementServiceDelegate { */ public static boolean isVMAliveOrHealthy(final AzureSlave slave) throws Exception { Configuration config = ServiceDelegateHelper.getConfiguration(slave); - String status = getVirtualMachineStatus(config, slave.getNodeName()); + String status = getVirtualMachineStatus(config, slave.getNodeName(), slave.getResourceGroupName()); LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: isVMAliveOrHealthy: status {0}", status); - // if VM status is DeletingVM/StoppedVM/StoppingRole/StoppingVM then consider VM to be not healthy - - return !(Constants.DELETING_VM_STATUS.equalsIgnoreCase(status) - || Constants.STOPPED_VM_STATUS.equalsIgnoreCase(status) - || Constants.STOPPING_VM_STATUS.equalsIgnoreCase(status) - || Constants.STOPPING_ROLE_STATUS.equalsIgnoreCase(status) - || Constants.STOPPED_DEALLOCATED_VM_STATUS.equalsIgnoreCase(status)); + return !(Constants.PROVISIONING_OR_DEPROVISIONING_VM_STATUS.equalsIgnoreCase(status) + || Constants.STOPPING_VM_STATUS.equalsIgnoreCase(status) + || Constants.STOPPED_VM_STATUS.equalsIgnoreCase(status) + || Constants.DEALLOCATED_VM_STATUS.equalsIgnoreCase(status)); } /** - * Retrieves count of virtual machine in a azure subscription. - * - * @param client - * @return + * Retrieves count of virtual machine in a azure subscription. This count + * is based off of the VMs that the current credential set has access to. It also + * does not deal with the classic, model. So keep this in mind. + * + * @param config Subscription configuration + * @return Total VM count * @throws Exception */ - public static int getVirtualMachineCount(final ComputeManagementClient client) throws Exception { + public static int getVirtualMachineCount(final Configuration config) throws Exception { try { - return client.getVirtualMachinesOperations().listAll(new ListParameters()).getVirtualMachines().size(); + ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(config); + VirtualMachineOperations vmOperations = client.getVirtualMachinesOperations(); + VirtualMachineListResponse response = vmOperations.listAll(new ListParameters()); + List virtualMachines = response.getVirtualMachines(); + return virtualMachines.size(); } catch (Exception e) { LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: getVirtualMachineCount: Got exception while getting hosted " @@ -660,45 +699,76 @@ public class AzureManagementServiceDelegate { * @param slave * @throws Exception */ - public static void shutdownVirtualMachine(final AzureSlave slave) throws Exception { - ServiceDelegateHelper.getComputeManagementClient(slave). - getVirtualMachinesOperations().powerOff(Constants.RESOURCE_GROUP_NAME, slave.getNodeName()); + public static void shutdownVirtualMachine(final AzureSlave slave) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: shutdownVirtualMachine: called for {0}", + slave.getNodeName() ); + + try { + ServiceDelegateHelper.getComputeManagementClient(slave). + getVirtualMachinesOperations().powerOff(slave.getResourceGroupName(), slave.getNodeName()); + } + catch (Exception e) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: provision: could not terminate or shutdown {0}, {1}", + new Object[] {slave.getNodeName(), e}); + } } /** * Deletes Azure virtual machine. * * @param slave - * @param sync + * @param doSynchronousTermination * @throws Exception */ - public static void terminateVirtualMachine(final AzureSlave slave, final boolean sync) throws Exception { - LOGGER.log(Level.INFO, "{0}: terminateVirtualMachine: called for {1} - asynchronous {2}", - new Object[] { AzureManagementServiceDelegate.class.getSimpleName(), slave.getDisplayName(), !sync }); - - if (sync) { - terminateVirtualMachine(slave); - } else { - ExecutionEngine.executeAsync(new Callable() { - - @Override - public Void call() throws Exception { - terminateVirtualMachine(slave); - return null; - } - }, new NoRetryStrategy()); - } + public static void terminateVirtualMachine(final AzureSlave slave) throws Exception { + final Configuration config = ServiceDelegateHelper.getConfiguration(slave); + terminateVirtualMachine(config, slave.getNodeName(), slave.getResourceGroupName()); } - private static void terminateVirtualMachine(final AzureSlave slave) throws Exception { + /** + * Terminates a virtual machine + * @param config Azure configuration + * @param vmName VM name + * @param resourceGroupName Resource group containing the VM + * @throws Exception + */ + public static void terminateVirtualMachine(final Configuration config, final String vmName, + final String resourceGroupName) throws Exception { try { - try { - if (slave != null && StringUtils.isNotBlank(slave.getNodeName()) && virtualMachineExists(slave)) { - final ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(slave); + if (virtualMachineExists(config, vmName, resourceGroupName)) { + final ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(config); - LOGGER.log(Level.INFO, "Remove virtual machine {0}", slave.getNodeName()); - client.getVirtualMachinesOperations().delete(Constants.RESOURCE_GROUP_NAME, slave.getNodeName()); + List diskUrisToRemove = new ArrayList(); + StorageProfile storageProfile = + client.getVirtualMachinesOperations().get(resourceGroupName, vmName).getVirtualMachine().getStorageProfile(); + // Remove the OS disks + diskUrisToRemove.add(new URI(storageProfile.getOSDisk().getVirtualHardDisk().getUri())); + // TODO: Remove data disks or add option to do so? + + // Remove the VM + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: terminateVirtualMachine: Removing virtual machine {0}", vmName); + client.getVirtualMachinesOperations().delete(resourceGroupName, vmName); + + // Now remove the disks + for (URI diskUri : diskUrisToRemove) { + // Obtain container, storage account, and blob name + String storageAccountName = diskUri.getHost().split("\\.")[0]; + String containerName = PathUtility.getContainerNameFromUri(diskUri, false); + String blobName = PathUtility.getBlobNameFromURI(diskUri, false); + + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: terminateVirtualMachine: Removing disk blob {0}, in container {1} of storage account {2}", + new Object [] { blobName, containerName, storageAccountName } ); + final StorageManagementClient storageClient = ServiceDelegateHelper.getStorageManagementClient(config); + StorageAccountKeys storageKeys = + storageClient.getStorageAccountsOperations().listKeys(resourceGroupName, storageAccountName).getStorageAccountKeys(); + URI blobURI = storageClient.getStorageAccountsOperations().getProperties(resourceGroupName, storageAccountName).getStorageAccount().getPrimaryEndpoints().getBlob(); + CloudBlobContainer container = StorageServiceDelegate.getBlobContainerReference( + storageAccountName, storageKeys.getKey1(), blobURI.toString(), containerName); + container.getBlockBlobReference(blobName).deleteIfExists(); + } + + // Also remove the init script (if it exists) } } catch (ExecutionException ee) { LOGGER.log(Level.INFO, @@ -718,12 +788,12 @@ public class AzureManagementServiceDelegate { throw se; } } finally { - LOGGER.log(Level.INFO, "Clean operation starting for {0} NIC and IP", slave.getNodeName()); + LOGGER.log(Level.INFO, "Clean operation starting for {0} NIC and IP", vmName); ExecutionEngine.executeAsync(new Callable() { @Override public Void call() throws Exception { - removeIPName(slave); + removeIPName(config, resourceGroupName, vmName); return null; } }, new NoRetryStrategy()); @@ -735,41 +805,47 @@ public class AzureManagementServiceDelegate { } } - private static void removeIPName(final AzureSlave slave) throws AzureCloudException { + /** + * Remove the IP name + * @param config + * @param resourceGroupName + * @param vmName + * @throws AzureCloudException + * We probably should record and pass in NIC/IP names. + * Also, if we go away from 1 public IP address per system, then we will need to update this. + * + */ + private static void removeIPName(final Configuration config, + final String resourceGroupName, final String vmName) throws AzureCloudException { + final NetworkResourceProviderClient client = ServiceDelegateHelper.getNetworkManagementClient(config); + + final String nic = vmName + "NIC"; try { - final NetworkResourceProviderClient client = ServiceDelegateHelper.getNetworkManagementClient(slave); + LOGGER.log(Level.INFO, "Remove NIC {0}", nic); + final NetworkInterfaceGetResponse obj + = client.getNetworkInterfacesOperations().get(resourceGroupName, nic); - final String nic = slave.getNodeName() + "NIC"; - try { - LOGGER.log(Level.INFO, "Remove NIC {0}", nic); - final NetworkInterfaceGetResponse obj - = client.getNetworkInterfacesOperations().get(Constants.RESOURCE_GROUP_NAME, nic); - - if (obj == null) { - LOGGER.log(Level.INFO, "NIC {0} already deprovisioned", nic); - } else { - client.getNetworkInterfacesOperations().delete(Constants.RESOURCE_GROUP_NAME, nic); - } - } catch (Exception ignore) { - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: removeIPName: while deleting NIC", ignore); + if (obj == null) { + LOGGER.log(Level.INFO, "NIC {0} already deprovisioned", nic); + } else { + client.getNetworkInterfacesOperations().delete(resourceGroupName, nic); } + } catch (Exception ignore) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: removeIPName: while deleting NIC", ignore); + } - final String ip = slave.getNodeName() + "IPName"; - try { - LOGGER.log(Level.INFO, "Remove IP {0}", ip); - final PublicIpAddressGetResponse obj - = client.getPublicIpAddressesOperations().get(Constants.RESOURCE_GROUP_NAME, ip); - if (obj == null) { - LOGGER.log(Level.INFO, "IP {0} already deprovisioned", ip); - } else { - client.getPublicIpAddressesOperations().delete(Constants.RESOURCE_GROUP_NAME, ip); - } - } catch (Exception ignore) { - LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: removeIPName: while deleting IPName", ignore); + final String ip = vmName + "IPName"; + try { + LOGGER.log(Level.INFO, "Remove IP {0}", ip); + final PublicIpAddressGetResponse obj + = client.getPublicIpAddressesOperations().get(resourceGroupName, ip); + if (obj == null) { + LOGGER.log(Level.INFO, "IP {0} already deprovisioned", ip); + } else { + client.getPublicIpAddressesOperations().delete(resourceGroupName, ip); } - } catch (UnrecoverableCloudException uce) { - LOGGER.log(Level.INFO, - "AzureManagementServiceDelegate: removeIPName: unrecoverable exception deleting IPName", uce); + } catch (Exception ignore) { + LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: removeIPName: while deleting IPName", ignore); } } @@ -781,7 +857,7 @@ public class AzureManagementServiceDelegate { */ public static void restartVirtualMachine(final AzureSlave slave) throws Exception { ServiceDelegateHelper.getComputeManagementClient(slave).getVirtualMachinesOperations(). - restart(Constants.RESOURCE_GROUP_NAME, slave.getNodeName()); + restart(slave.getResourceGroupName(), slave.getNodeName()); } /** @@ -799,7 +875,7 @@ public class AzureManagementServiceDelegate { while (!successful) { try { - client.getVirtualMachinesOperations().start(Constants.RESOURCE_GROUP_NAME, slave.getNodeName()); + client.getVirtualMachinesOperations().start(slave.getResourceGroupName(), slave.getNodeName()); successful = true; // may be we can just return } catch (Exception e) { LOGGER.log(Level.INFO, "AzureManagementServiceDelegate: startVirtualMachine: got exception while " @@ -823,12 +899,12 @@ public class AzureManagementServiceDelegate { * @return */ private static VirtualNetwork getVirtualNetwork( - final Configuration config, final String virtualNetworkName) { + final Configuration config, final String virtualNetworkName, final String resourceGroupName) { try { final NetworkResourceProviderClient client = ServiceDelegateHelper.getNetworkManagementClient(config); final VirtualNetworkListResponse listResponse - = client.getVirtualNetworksOperations().list(Constants.RESOURCE_GROUP_NAME); + = client.getVirtualNetworksOperations().list(resourceGroupName); if (listResponse != null) { for (VirtualNetwork vnet : listResponse.getVirtualNetworks()) { @@ -843,6 +919,19 @@ public class AzureManagementServiceDelegate { } return null; } + + /** + * Gets a final location name from a display name location. + * @param location + * @return + */ + private static String getLocationName(String location) { + if (AVAILABLE_LOCATIONS_ALL.containsKey(location)) { + return AVAILABLE_LOCATIONS_ALL.get(location); + } + + return null; + } /** * Verifies template configuration by making server calls if needed. @@ -852,7 +941,6 @@ public class AzureManagementServiceDelegate { * @param clientSecret * @param oauth2TokenEndpoint * @param serviceManagementURL - * @param maxVirtualMachinesLimit * @param templateName * @param labels * @param location @@ -875,6 +963,7 @@ public class AzureManagementServiceDelegate { * @param templateStatus * @param jvmOptions * @param returnOnSingleError + * @param resourceGroupName * @return */ public static List verifyTemplate( @@ -883,7 +972,6 @@ public class AzureManagementServiceDelegate { final String clientSecret, final String oauth2TokenEndpoint, final String serviceManagementURL, - final String maxVirtualMachinesLimit, final String templateName, final String labels, final String location, @@ -903,37 +991,28 @@ public class AzureManagementServiceDelegate { final String virtualNetworkName, final String subnetName, final String retentionTimeInMin, - // final String cloudServiceName, - final String templateStatus, final String jvmOptions, + final String resourceGroupName, final boolean returnOnSingleError) { List errors = new ArrayList(); Configuration config = null; - + // Load configuration try { config = ServiceDelegateHelper.loadConfiguration( subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL); - - // Verify if profile configuration is valid - String validationResult = verifyAzureProfileConfiguration( - config, subscriptionId, clientId, clientSecret, oauth2TokenEndpoint); - if (!validationResult.equalsIgnoreCase(Constants.OP_SUCCESS)) { - errors.add(validationResult); - // If profile validation failed , no point in validating rest of the field , just return error + String validationResult; + + // Verify basic info about the template + + //Verify number of parallel jobs + validationResult = verifyNoOfExecutors(noOfParallelJobs); + addValidationResultIfFailed(validationResult, errors); + if (returnOnSingleError && errors.size() > 0) { return errors; } - - //Verify number of parallel jobs - if (returnOnSingleError) { - validationResult = verifyNoOfExecutors(noOfParallelJobs); - addValidationResultIfFailed(validationResult, errors); - if (returnOnSingleError && errors.size() > 0) { - return errors; - } - } - + validationResult = verifyRetentionTime(retentionTimeInMin); addValidationResultIfFailed(validationResult, errors); if (returnOnSingleError && errors.size() > 0) { @@ -954,7 +1033,13 @@ public class AzureManagementServiceDelegate { return errors; } - validationResult = verifyImage(image, osType, imagePublisher, imageOffer, imageSku, imageVersion); + validationResult = verifyImageParameters(image, osType, imagePublisher, imageOffer, imageSku, imageVersion); + addValidationResultIfFailed(validationResult, errors); + if (returnOnSingleError && errors.size() > 0) { + return errors; + } + + validationResult = verifyLocation(location, serviceManagementURL); addValidationResultIfFailed(validationResult, errors); if (returnOnSingleError && errors.size() > 0) { return errors; @@ -962,21 +1047,17 @@ public class AzureManagementServiceDelegate { verifyTemplateAsync( config, - templateName, - maxVirtualMachinesLimit, location, image, - osType, imagePublisher, imageOffer, imageSku, imageVersion, - slaveLaunchMethod, storageAccountName, virtualNetworkName, subnetName, - errors, - returnOnSingleError + resourceGroupName, + errors ); } catch (Exception e) { @@ -989,41 +1070,26 @@ public class AzureManagementServiceDelegate { private static void verifyTemplateAsync( final Configuration config, - final String templateName, - final String maxVirtualMachinesLimit, final String location, final String image, - final String osType, final String imagePublisher, final String imageOffer, final String imageSku, final String imageVersion, - final String slaveLaunchMethod, final String storageAccountName, final String virtualNetworkName, final String subnetName, - final List errors, - final boolean returnOnSingleError) { + final String resourceGroupName, + final List errors) { List> verificationTaskList = new ArrayList>(); - // Callable for max virtual limit - if (returnOnSingleError) { - Callable callVerifyMaxVirtualMachineLimit = new Callable() { - - @Override - public String call() throws Exception { - return verifyMaxVirtualMachineLimit(config, maxVirtualMachinesLimit); - } - }; - verificationTaskList.add(callVerifyMaxVirtualMachineLimit); - } // Callable for virtual network. Callable callVerifyVirtualNetwork = new Callable() { @Override public String call() throws Exception { - return verifyVirtualNetwork(config, virtualNetworkName, subnetName); + return verifyVirtualNetwork(config, virtualNetworkName, subnetName, resourceGroupName); } }; verificationTaskList.add(callVerifyVirtualNetwork); @@ -1034,7 +1100,7 @@ public class AzureManagementServiceDelegate { @Override public String call() throws Exception { return verifyVirtualMachineImage(config, - image, location, imagePublisher, imageOffer, imageSku, imageVersion); + location, storageAccountName, image, imagePublisher, imageOffer, imageSku, imageVersion); } }; verificationTaskList.add(callVerifyVirtualMachineImage); @@ -1064,65 +1130,6 @@ public class AzureManagementServiceDelegate { } } - private static String verifyAzureProfileConfiguration( - final Configuration config, - final String subscriptionId, - final String clientId, - final String clientSecret, - final String oauth2TokenEndpoint) { - - if (StringUtils.isBlank(subscriptionId) - || StringUtils.isBlank(clientId) - || StringUtils.isBlank(oauth2TokenEndpoint) - || StringUtils.isBlank(clientSecret)) { - - return Messages.Azure_GC_Template_Val_Profile_Missing(); - } else { - if (!verifyConfiguration(config).equals(Constants.OP_SUCCESS)) { - return Messages.Azure_GC_Template_Val_Profile_Err(); - } - } - return Constants.OP_SUCCESS; - } - - private static String verifyMaxVirtualMachineLimit( - final Configuration config, final String maxVirtualMachinesLimit) { - - boolean considerDefaultVMLimit = false; - int maxVMs = 0; - if (StringUtils.isBlank(maxVirtualMachinesLimit)) { - considerDefaultVMLimit = true; - } else { - try { - maxVMs = Integer.parseInt(maxVirtualMachinesLimit); - - if (maxVMs <= 0) { - considerDefaultVMLimit = true; - } - } catch (Exception e) { - considerDefaultVMLimit = true; - } - } - - ComputeManagementClient client = ServiceDelegateHelper.getComputeManagementClient(config); - maxVMs = considerDefaultVMLimit ? Constants.DEFAULT_MAX_VM_LIMIT : maxVMs; - try { - int currentCount = getVirtualMachineCount(client); - - if (currentCount < maxVMs) { - return Constants.OP_SUCCESS; - } else { - if (considerDefaultVMLimit) { - return Messages.Azure_GC_Template_max_VM_Err(currentCount, Constants.DEFAULT_MAX_VM_LIMIT); - } else { - return Messages.Azure_GC_Template_max_VM_Err(currentCount, maxVirtualMachinesLimit); - } - } - } catch (Exception e) { - return ("Exception occured while validating max virtual machines limit value"); - } - } - public static String verifyNoOfExecutors(final String noOfExecutors) { try { if (StringUtils.isBlank(noOfExecutors)) { @@ -1152,9 +1159,10 @@ public class AzureManagementServiceDelegate { private static String verifyVirtualNetwork( final Configuration config, final String virtualNetworkName, - final String subnetName) { + final String subnetName, + final String resourceGroupName) { if (StringUtils.isNotBlank(virtualNetworkName)) { - VirtualNetwork virtualNetwork = getVirtualNetwork(config, virtualNetworkName); + VirtualNetwork virtualNetwork = getVirtualNetwork(config, virtualNetworkName, resourceGroupName); if (virtualNetwork == null) { return Messages.Azure_GC_Template_VirtualNetwork_NotFound(virtualNetworkName); @@ -1185,32 +1193,72 @@ public class AzureManagementServiceDelegate { private static String verifyVirtualMachineImage( final Configuration config, final String location, + final String storageAccountName, final String image, final String imagePublisher, final String imageOffer, final String imageSku, final String imageVersion) { - try { - if (StringUtils.isNotBlank(image)) { - // to be defined - } else { + if (StringUtils.isNotBlank(image)) { + try { + // Custom image verification. We must verify that the VM image + // storage account is the same as the target storage account. + // The URI for he storage account should be https://. + // Parse that out and verify agaisnt the image storageAccountName + + // Check that the image string is a URI by attempting to create + // a URI + final URI u; + try { + u = URI.create(image); + } catch (Exception e) { + return Messages.Azure_GC_Template_ImageURI_Not_Valid(); + } + String host = u.getHost(); + // storage account name is the first element of the host + int firstDot = host.indexOf('.'); + if (firstDot == -1) { + // This in an unexpected URI + return Messages.Azure_GC_Template_ImageURI_Not_Valid(); + } + String uriStorageAccount = host.substring(0, firstDot); + if (!uriStorageAccount.equals(storageAccountName)) { + return Messages.Azure_GC_Template_ImageURI_Not_Valid(); + } + return Constants.OP_SUCCESS; + } + catch (Exception e) { + LOGGER.log(Level.SEVERE, "Invalid virtual machine image", e); + return Messages.Azure_GC_Template_ImageURI_Not_Valid(); + } + } else { + try { final VirtualMachineImageOperations client = ServiceDelegateHelper.getComputeManagementClient(config).getVirtualMachineImagesOperations(); final VirtualMachineImageGetParameters params = new VirtualMachineImageGetParameters(); - params.setLocation(location); + params.setLocation(getLocationName(location)); params.setPublisherName(imagePublisher); params.setOffer(imageOffer); params.setSkus(imageSku); - params.setVersion(imageVersion); - + // The image version in Azure is an encoded date. 'latest' (our default) + // is not valid in this API request, though can be used in the + // the image template. If the image version is 'latest', clear + // to the empty string + if (imageVersion.equalsIgnoreCase("latest")) { + params.setVersion(""); + } + else { + params.setVersion(imageVersion); + } + client.get(params); } - + catch (Exception e) { + LOGGER.log(Level.SEVERE, "Invalid virtual machine image", e); + return Messages.Azure_GC_Template_ImageReference_Not_Valid(e.getMessage()); + } return Constants.OP_SUCCESS; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Invalid virtual machine image", e); - return Messages.Azure_GC_Template_ImageFamilyOrID_Not_Valid(); } } @@ -1233,22 +1281,56 @@ public class AzureManagementServiceDelegate { return Messages.Azure_GC_JVM_Option_Err(); } } + + /** + * Check the location. This location is the display name. + * @param location + * @return + */ + private static String verifyLocation(final String location, final String serviceManagementURL) { + String locationName = getLocationName(location); + if (locationName != null) { + return Constants.OP_SUCCESS; + } else { + return Messages.Azure_GC_Template_LOC_Not_Found(); + } + } - private static String verifyImage( + /** + * Verify the validity of the image parameters (does not verify actual values) + * @param image + * @param osType + * @param imagePublisher + * @param imageOffer + * @param imageSku + * @param imageVersion + * @return + */ + private static String verifyImageParameters( final String image, final String osType, final String imagePublisher, final String imageOffer, final String imageSku, final String imageVersion) { - if ((StringUtils.isNotBlank(image) && StringUtils.isNotBlank(osType)) - || (StringUtils.isNotBlank(imagePublisher) + if ((StringUtils.isNotBlank(image) && StringUtils.isNotBlank(osType))) { + // Check that the image string is a URI by attempting to create + // a URI + final URI u; + try { + u = URI.create(image); + } catch (Exception e) { + Messages.Azure_GC_Template_ImageURI_Not_Valid(); + } + return Constants.OP_SUCCESS; + } + else if (StringUtils.isNotBlank(imagePublisher) && StringUtils.isNotBlank(imageOffer) && StringUtils.isNotBlank(imageSku) - && StringUtils.isNotBlank(imageVersion))) { + && StringUtils.isNotBlank(imageVersion)) { return Constants.OP_SUCCESS; } else { - return Messages.Azure_GC_Template_ImageFamilyOrID_Not_Valid(); + return Messages.Azure_GC_Template_ImageReference_Not_Valid("Image parameters should not be blank."); } } } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureSlave.java b/src/main/java/com/microsoftopentechnologies/azure/AzureSlave.java index 25ae0c4..c1c58d9 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureSlave.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureSlave.java @@ -25,6 +25,7 @@ import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import com.microsoftopentechnologies.azure.util.Constants; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import com.microsoftopentechnologies.azure.util.FailureStage; import com.microsoftopentechnologies.azure.remote.AzureSSHLauncher; @@ -39,6 +40,7 @@ import hudson.slaves.ComputerLauncher; import hudson.slaves.OfflineCause; import hudson.slaves.RetentionStrategy; import java.util.logging.Level; +import org.jvnet.localizer.Localizable; public class AzureSlave extends AbstractCloudSlave { @@ -87,10 +89,20 @@ public class AzureSlave extends AbstractCloudSlave { private String templateName; - private boolean deleteSlave; + private CleanUpAction cleanUpAction; + + private Localizable cleanUpReason; + + private String resourceGroupName; private static final Logger LOGGER = Logger.getLogger(AzureSlave.class.getName()); + private final boolean executeInitScriptAsRoot; + + private final boolean doNotUseMachineIfInitFails; + + private boolean eligibleForReuse; + @DataBoundConstructor public AzureSlave( final String name, @@ -111,6 +123,7 @@ public class AzureSlave extends AbstractCloudSlave { final String adminPassword, final String jvmOptions, final boolean shutdownOnIdle, + final boolean eligibleForReuse, final String deploymentName, final int retentionTimeInMin, final String initScript, @@ -120,7 +133,11 @@ public class AzureSlave extends AbstractCloudSlave { final String oauth2TokenEndpoint, final String managementURL, final String slaveLaunchMethod, - final boolean deleteSlave) throws FormException, IOException { + final CleanUpAction cleanUpAction, + final Localizable cleanUpReason, + final String resourceGroupName, + final boolean executeInitScriptAsRoot, + final boolean doNotUseMachineIfInitFails) throws FormException, IOException { super(name, nodeDescription, remoteFS, numExecutors, mode, label, launcher, retentionStrategy, nodeProperties); @@ -132,6 +149,7 @@ public class AzureSlave extends AbstractCloudSlave { this.adminPassword = adminPassword; this.jvmOptions = jvmOptions; this.shutdownOnIdle = shutdownOnIdle; + this.eligibleForReuse = eligibleForReuse; this.deploymentName = deploymentName; this.retentionTimeInMin = retentionTimeInMin; this.initScript = initScript; @@ -143,7 +161,11 @@ public class AzureSlave extends AbstractCloudSlave { this.oauth2TokenEndpoint = oauth2TokenEndpoint; this.managementURL = managementURL; this.slaveLaunchMethod = slaveLaunchMethod; - this.deleteSlave = deleteSlave; + this.setCleanUpAction(cleanUpAction); + this.setCleanupReason(cleanUpReason); + this.resourceGroupName = resourceGroupName; + this.executeInitScriptAsRoot = executeInitScriptAsRoot; + this.doNotUseMachineIfInitFails = doNotUseMachineIfInitFails; } public AzureSlave( @@ -162,6 +184,7 @@ public class AzureSlave extends AbstractCloudSlave { final String adminPassword, final String jvmOptions, final boolean shutdownOnIdle, + final boolean eligibleForReuse, final String deploymentName, final int retentionTimeInMin, final String initScript, @@ -171,7 +194,11 @@ public class AzureSlave extends AbstractCloudSlave { final String oauth2TokenEndpoint, final String managementURL, final String slaveLaunchMethod, - final boolean deleteSlave) throws FormException, IOException { + final CleanUpAction cleanUpAction, + final Localizable cleanUpReason, + final String resourceGroupName, + final boolean executeInitScriptAsRoot, + final boolean doNotUseMachineIfInitFails) throws FormException, IOException { this(name, templateName, @@ -181,11 +208,8 @@ public class AzureSlave extends AbstractCloudSlave { numExecutors, mode, label, - slaveLaunchMethod.equalsIgnoreCase("SSH") - ? osType.equalsIgnoreCase("Windows") - ? new AzureSSHLauncher() - : new AzureSSHLauncher() - : new JNLPLauncher(), + slaveLaunchMethod.equalsIgnoreCase("SSH") ? + new AzureSSHLauncher() : new JNLPLauncher(), new AzureCloudRetensionStrategy(retentionTimeInMin), Collections.>emptyList(), cloudName, @@ -195,7 +219,7 @@ public class AzureSlave extends AbstractCloudSlave { adminPassword, jvmOptions, shutdownOnIdle, - // cloudServiceName, + eligibleForReuse, deploymentName, retentionTimeInMin, initScript, @@ -205,7 +229,11 @@ public class AzureSlave extends AbstractCloudSlave { oauth2TokenEndpoint, managementURL, slaveLaunchMethod, - deleteSlave); + cleanUpAction, + cleanUpReason, + resourceGroupName, + executeInitScriptAsRoot, + doNotUseMachineIfInitFails); } public String getCloudName() { @@ -261,12 +289,71 @@ public class AzureSlave extends AbstractCloudSlave { return adminPassword; } - public boolean isDeleteSlave() { - return deleteSlave; + public CleanUpAction getCleanUpAction() { + return cleanUpAction; + } + + public Localizable getCleanUpReason() { + return cleanUpReason; + } + + /** + * @param cleanUpReason + */ + private void setCleanUpAction(CleanUpAction cleanUpAction) { + // Translate a default cleanup action into what we want for a particular + // node + if (cleanUpAction == CleanUpAction.DEFAULT) { + if (isShutdownOnIdle()) { + cleanUpAction = CleanUpAction.SHUTDOWN; + } + else { + cleanUpAction = CleanUpAction.DELETE; + } + } + this.cleanUpAction = cleanUpAction; + } + + /** + * @param cleanUpReason + */ + private void setCleanupReason(Localizable cleanUpReason) { + this.cleanUpReason = cleanUpReason; + } + + /** + * Clear the cleanup action and reset to the default behavior + */ + public void clearCleanUpAction() { + setCleanUpAction(CleanUpAction.DEFAULT); + setCleanupReason(null); + } + + /** + * Block any cleanup from happening + */ + public void blockCleanUpAction() { + setCleanUpAction(CleanUpAction.BLOCK); + setCleanupReason(null); + } + + public boolean isCleanUpBlocked() { + return getCleanUpAction() == CleanUpAction.BLOCK; } - public void setDeleteSlave(boolean deleteSlave) { - this.deleteSlave = deleteSlave; + public void setCleanUpAction(CleanUpAction cleanUpAction, Localizable cleanUpReason) { + if (cleanUpAction != CleanUpAction.DELETE && cleanUpAction != CleanUpAction.SHUTDOWN) { + throw new IllegalStateException("Only use this method to set explicit cleanup operations"); + } + if (this.toComputer()!= null) { + AzureComputer computer = (AzureComputer)this.toComputer(); + // Set the machine temporarily offline machine with an offline reason. + computer.setTemporarilyOffline(true, OfflineCause.create(cleanUpReason)); + // Reset the "by user" bit. + computer.setSetOfflineByUser(false); + } + setCleanUpAction(cleanUpAction); + setCleanupReason(cleanUpReason); } public String getJvmOptions() { @@ -281,6 +368,14 @@ public class AzureSlave extends AbstractCloudSlave { this.shutdownOnIdle = shutdownOnIdle; } + public boolean isEligibleForReuse() { + return eligibleForReuse; + } + + public void setEligibleForReuse(boolean eligibleForReuse) { + this.eligibleForReuse = eligibleForReuse; + } + public String getPublicDNSName() { return publicDNSName; } @@ -316,6 +411,18 @@ public class AzureSlave extends AbstractCloudSlave { public void setTemplateName(String templateName) { this.templateName = templateName; } + + public String getResourceGroupName() { + return resourceGroupName; + } + + public boolean getExecuteInitScriptAsRoot() { + return executeInitScriptAsRoot; + } + + public boolean getDoNotUseMachineIfInitFails() { + return doNotUseMachineIfInitFails; + } @Override protected void _terminate(final TaskListener arg0) throws IOException, InterruptedException { @@ -329,35 +436,37 @@ public class AzureSlave extends AbstractCloudSlave { return new AzureComputer(this); } - public void idleTimeout() throws Exception { - if (shutdownOnIdle) { - // Call shutdown only if the slave is online - if (this.getComputer().isOnline()) { - LOGGER.log(Level.INFO, "AzureSlave: idleTimeout: shutdownOnIdle is true, shutting down slave {0}", this. - getDisplayName()); - this.getComputer().disconnect(OfflineCause.create(Messages._IDLE_TIMEOUT_SHUTDOWN())); - AzureManagementServiceDelegate.shutdownVirtualMachine(this); - setDeleteSlave(false); - } - } else { - LOGGER.log(Level.INFO, - "AzureSlave: idleTimeout: shutdownOnIdle is false, deleting slave {0}", this.getDisplayName()); - setDeleteSlave(true); - AzureManagementServiceDelegate.terminateVirtualMachine(this, true); - Jenkins.getInstance().removeNode(this); - } - } - public AzureCloud getCloud() { return (AzureCloud) Jenkins.getInstance().getCloud(cloudName); } + + public void shutdown(Localizable reason) { + LOGGER.log(Level.INFO, "AzureSlave: shutdown: shutting down slave {0}", this. + getDisplayName()); + this.getComputer().setAcceptingTasks(false); + this.getComputer().disconnect(OfflineCause.create(reason)); + AzureManagementServiceDelegate.shutdownVirtualMachine(this); + // After shutting down succesfully, set the node as eligible for + // reuse. + setEligibleForReuse(true); + } - public void deprovision() throws Exception { + /** + * Delete node in Azure and in Jenkins + * @throws Exception + */ + public void deprovision(Localizable reason) throws Exception { LOGGER.log(Level.INFO, "AzureSlave: deprovision: Deprovision called for slave {0}", this.getDisplayName()); - AzureManagementServiceDelegate.terminateVirtualMachine(this, true); + this.getComputer().setAcceptingTasks(false); + this.getComputer().disconnect(OfflineCause.create(reason)); + AzureManagementServiceDelegate.terminateVirtualMachine(this); LOGGER.log(Level.INFO, "AzureSlave: deprovision: {0} has been deprovisioned. Remove node ...", this.getDisplayName()); - setDeleteSlave(true); + // Adjust parent VM count up by one. + AzureCloud parentCloud = getCloud(); + if (parentCloud != null) { + parentCloud.adjustVirtualMachineCount(1); + } Jenkins.getInstance().removeNode(this); } @@ -365,13 +474,6 @@ public class AzureSlave extends AbstractCloudSlave { return AzureManagementServiceDelegate.isVMAliveOrHealthy(this); } - public void setTemplateStatus(String templateStatus, String templateStatusDetails) { - AzureCloud azureCloud = getCloud(); - AzureSlaveTemplate slaveTemplate = azureCloud.getAzureSlaveTemplate(templateName); - - slaveTemplate.handleTemplateStatus(templateStatusDetails, FailureStage.POSTPROVISIONING, this); - } - @Override public String toString() { return "AzureSlave [" @@ -396,7 +498,7 @@ public class AzureSlave extends AbstractCloudSlave { + "\n\toauth2TokenEndpoint=" + oauth2TokenEndpoint + "\n\tmanagementURL=" + managementURL + "\n\ttemplateName=" + templateName - + "\n\tdeleteSlave=" + deleteSlave + + "\n\tcleanUpAction=" + cleanUpAction + "\n]"; } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveCleanUpTask.java b/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveCleanUpTask.java index d1b5038..7cf4774 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveCleanUpTask.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveCleanUpTask.java @@ -22,6 +22,7 @@ import java.util.logging.Logger; import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; import com.microsoftopentechnologies.azure.retry.DefaultRetryStrategy; import com.microsoftopentechnologies.azure.util.ExecutionEngine; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import jenkins.model.Jenkins; import hudson.Extension; @@ -46,32 +47,82 @@ public final class AzureSlaveCleanUpTask extends AsyncPeriodicWork { AzureComputer azureComputer = (AzureComputer) computer; final AzureSlave slaveNode = azureComputer.getNode(); - if (azureComputer.isOffline() && slaveNode.isDeleteSlave()) { - if (AzureManagementServiceDelegate.virtualMachineExists(slaveNode)) { - Callable task = new Callable() { - - @Override - public Void call() throws Exception { - slaveNode.idleTimeout(); - return null; - } - }; - - try { - ExecutionEngine.executeWithRetry(task, new DefaultRetryStrategy( - 3, // max retries - 10, // Default backoff in seconds - 30 * 60 // Max timeout in seconds - )); - } catch (AzureCloudException exception) { - // No need to throw exception back, just log and move on. + // If the machine is not offline, then don't do anything. + if (!azureComputer.isOffline()) { + continue; + } + + // If the machine is not idle, don't do anything. + // Could have been taken offline by the plugin while still running + // builds. + if (!azureComputer.isIdle()) { + continue; + } + + // Even if offline, a machine that has been temporarily marked offline + // should stay (this could be for investigation). + if (azureComputer.isSetOfflineByUser()) { + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: node {0} was set offline by user, skipping", slaveNode.getDisplayName()); + continue; + } + + // If the machine is in "keep" state, skip + if (slaveNode.isCleanUpBlocked()) { + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: node {0} blocked to cleanup", slaveNode.getDisplayName()); + continue; + } + + // Check if the virtual machine exists. If not, it could have been + // deleted in the background. Remove from Jenkins if that is the case. + if (!AzureManagementServiceDelegate.virtualMachineExists(slaveNode)) { + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: node {0} doesn't exist, removing", slaveNode.getDisplayName()); + Jenkins.getInstance().removeNode(slaveNode); + continue; + } + + // Machine exists but is in either DELETE or SHUTDOWN state. + // Execute that action. + Callable task = new Callable() { + @Override + public Void call() throws Exception { + // Depending on the cleanup action, run the appropriate + if (slaveNode.getCleanUpAction() == CleanUpAction.DELETE) { LOGGER.log(Level.INFO, - "AzureSlaveCleanUpTask: execute: failed to remove " + slaveNode.getDisplayName(), - exception); + "AzureSlaveCleanUpTask: execute: deleting {0}", slaveNode.getDisplayName()); + slaveNode.deprovision(slaveNode.getCleanUpReason()); } - } else { - Jenkins.getInstance().removeNode(slaveNode); + else if(slaveNode.getCleanUpAction() == CleanUpAction.SHUTDOWN) { + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: shutting down {0}", slaveNode.getDisplayName()); + slaveNode.shutdown(slaveNode.getCleanUpReason()); + // We shut down the slave properly. Mark the slave + // as "KEEP" so that it doesn't get deleted. + slaveNode.blockCleanUpAction(); + } + else { + throw new IllegalStateException("Unknown cleanup action"); + } + return null; } + }; + + try { + ExecutionEngine.executeAsync(task, new DefaultRetryStrategy( + 3, // max retries + 10, // Default backoff in seconds + 30 * 60 // Max timeout in seconds + )); + } catch (AzureCloudException exception) { + // No need to throw exception back, just log and move on. + LOGGER.log(Level.INFO, + "AzureSlaveCleanUpTask: execute: failed to shutdown/delete " + slaveNode.getDisplayName(), + exception); + // In case the node had a non-delete cleanup action before, + // set the cleanup action to delete + slaveNode.setCleanUpAction(CleanUpAction.DELETE, Messages._Failed_Initial_Shutdown_Or_Delete()); } } } @@ -79,7 +130,7 @@ public final class AzureSlaveCleanUpTask extends AsyncPeriodicWork { @Override public long getRecurrencePeriod() { - // Every 5 minutes + // Every 15 minutes return 15 * 60 * 1000; } } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction.java b/src/main/java/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction.java index b5f13d9..aae5373 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction.java @@ -15,6 +15,8 @@ */ package com.microsoftopentechnologies.azure; +import static com.microsoftopentechnologies.azure.Messages._Build_Action_Shutdown_Slave; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import java.io.IOException; import java.util.logging.Logger; @@ -28,6 +30,7 @@ import hudson.model.Computer; import hudson.model.Node; import hudson.model.BuildListener; import hudson.model.Result; +import hudson.slaves.OfflineCause; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; @@ -51,49 +54,46 @@ public class AzureSlavePostBuildAction extends Recorder { @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + Computer computer = Computer.currentComputer(); + Node node = computer.getNode(); - LOGGER.log(Level.INFO, - "AzureSlavePostBuildAction: perform: build is not successful , taking post build action {0} for slave ", - slavePostBuildAction); - 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.Build_Action_Shutdown_Slave().equalsIgnoreCase(slavePostBuildAction)) { - slave.setShutdownOnIdle(true); - slave.idleTimeout(); - } else if (Messages.Build_Action_Delete_Slave().equalsIgnoreCase(slavePostBuildAction) - || (Messages.Build_Action_Delete_Slave_If_Not_Success().equalsIgnoreCase( - slavePostBuildAction) && build.getResult() != Result.SUCCESS)) { - slave.setShutdownOnIdle(false); - slave.idleTimeout(); - } - } - successfull = true; - } catch (Exception e) { - retryCount++; - LOGGER.log(Level.INFO, "AzureSlavePostBuildAction: perform: Exception occured while {0}" + "\n" - + "Will retry again after 30 seconds. Current retry count {1}" - + "\n" + "Error code {2}", new Object[] { slavePostBuildAction, retryCount, 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) { - // ignore - } - } + if (!(node instanceof AzureSlave)) { + // We don't own this node. Nothing to do. + return true; } - + + AzureSlave slave = (AzureSlave)node; + AzureComputer azureComputer = (AzureComputer)computer; + LOGGER.log(Level.INFO, + "AzureSlavePostBuildAction: perform: build action {0} for slave {1}", + new Object [] { slavePostBuildAction, slave.getNodeName() }); + + // If the node has been taken offline by the user, skip the postbuild task. + if (azureComputer.isSetOfflineByUser()) { + LOGGER.log(Level.INFO, + "AzureSlavePostBuildAction: perform: slave {0} was taken offline by user, skipping postbuild", + slave.getNodeName()); + return true; + } + + azureComputer.setAcceptingTasks(false); + + // The post build action cannot immediately delete the node + // Doing so would cause the post build action to show some wacky errors + // and potentially fail. We also don't want to delete the machine if + // other stuff was running at the moment. The cleanup action will set the machine + // offline and it will + if (Messages.Build_Action_Shutdown_Slave().equalsIgnoreCase(slavePostBuildAction)) { + slave.setCleanUpAction(CleanUpAction.SHUTDOWN, Messages._Build_Action_Shutdown_Slave()); + } + else if (Messages.Build_Action_Delete_Slave_If_Not_Success().equalsIgnoreCase( + slavePostBuildAction) && (build.getResult() != Result.SUCCESS)) { + slave.setCleanUpAction(CleanUpAction.DELETE, Messages._Build_Action_Delete_Slave_If_Not_Success()); + } + else if (Messages.Build_Action_Delete_Slave().equalsIgnoreCase(slavePostBuildAction)) { + slave.setCleanUpAction(CleanUpAction.DELETE, Messages._Build_Action_Delete_Slave()); + } + return true; } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveTemplate.java b/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveTemplate.java index 735d437..1749153 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveTemplate.java +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureSlaveTemplate.java @@ -17,11 +17,8 @@ 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.logging.Logger; import javax.servlet.ServletException; @@ -31,11 +28,6 @@ import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; -import com.microsoftopentechnologies.azure.exceptions.AzureCloudException; -import com.microsoftopentechnologies.azure.retry.DefaultRetryStrategy; -import com.microsoftopentechnologies.azure.retry.LinearRetryForAllExceptions; -import com.microsoftopentechnologies.azure.util.ExecutionEngine; -import com.microsoft.windowsazure.Configuration; import com.microsoftopentechnologies.azure.util.AzureUtil; import com.microsoftopentechnologies.azure.util.Constants; import com.microsoftopentechnologies.azure.util.FailureStage; @@ -50,6 +42,7 @@ import hudson.model.Node; import hudson.model.labels.LabelAtom; import hudson.util.FormValidation; import hudson.util.ListBoxModel; +import java.util.Map; import java.util.logging.Level; import org.apache.commons.lang.StringUtils; @@ -115,13 +108,21 @@ public class AzureSlaveTemplate implements Describable { private final String jvmOptions; - private String templateStatus; + // Indicates whether the template is disabled. + // If disabled, will not attempt to verify or use + private boolean templateDisabled; private String templateStatusDetails; public transient AzureCloud azureCloud; private transient Set labelDataSet; + + private boolean templateVerified; + + private boolean executeInitScriptAsRoot; + + private boolean doNotUseMachineIfInitFails; @DataBoundConstructor public AzureSlaveTemplate( @@ -151,8 +152,10 @@ public class AzureSlaveTemplate implements Describable { final String jvmOptions, final String retentionTimeInMin, final boolean shutdownOnIdle, - final String templateStatus, - final String templateStatusDetails) { + final boolean templateDisabled, + final String templateStatusDetails, + final boolean executeInitScriptAsRoot, + final boolean doNotUseMachineIfInitFails) { this.templateName = templateName; this.templateDesc = templateDesc; this.labels = labels; @@ -183,19 +186,19 @@ public class AzureSlaveTemplate implements Describable { this.subnetName = subnetName; this.slaveWorkSpace = slaveWorkSpace; this.jvmOptions = jvmOptions; + this.executeInitScriptAsRoot = executeInitScriptAsRoot; + this.doNotUseMachineIfInitFails = doNotUseMachineIfInitFails; if (StringUtils.isBlank(retentionTimeInMin) || !retentionTimeInMin.matches(Constants.REG_EX_DIGIT)) { this.retentionTimeInMin = Constants.DEFAULT_IDLE_TIME; } else { this.retentionTimeInMin = Integer.parseInt(retentionTimeInMin); } - this.templateStatus = templateStatus; - - if (templateStatus.equalsIgnoreCase(Constants.TEMPLATE_STATUS_ACTIVE)) { - this.templateStatusDetails = ""; - } else { - this.templateStatusDetails = templateStatusDetails; - } + this.templateDisabled = templateDisabled; + this.templateStatusDetails = ""; + // Reset the template verification status. + this.templateVerified = false; + // Forms data which is not persisted readResolve(); } @@ -325,12 +328,29 @@ public class AzureSlaveTemplate implements Describable { return slaveLaunchMethod; } - public void setTemplateStatus(String templateStatus) { - this.templateStatus = templateStatus; + /** + * Returns true if this template is disabled and cannot be used, + * false otherwise. + * @return True/false + */ + public boolean isTemplateDisabled() { + return this.templateDisabled; } - - public String getTemplateStatus() { - return templateStatus; + + /** + * Is the template set up and verified? + * @return True if the template is set up and verified, false otherwise. + */ + public boolean isTemplateVerified() { + return templateVerified; + } + + /** + * Set the template verification status + * @param isValid True for verified + valid, false otherwise. + */ + public void setTemplateVerified(boolean isValid) { + templateVerified = isValid; } public String getTemplateStatusDetails() { @@ -340,6 +360,27 @@ public class AzureSlaveTemplate implements Describable { public void setTemplateStatusDetails(String templateStatusDetails) { this.templateStatusDetails = templateStatusDetails; } + + public String getResourceGroupName() { + // Allow overriding? + return getAzureCloud().getResourceGroupName(); + } + + public boolean getExecuteInitScriptAsRoot() { + return executeInitScriptAsRoot; + } + + public void setExecuteInitScriptAsRoot(boolean executeAsRoot) { + executeInitScriptAsRoot = executeAsRoot; + } + + public boolean getDoNotUseMachineIfInitFails() { + return doNotUseMachineIfInitFails; + } + + public void setDoNotUseMachineIfInitFails(boolean doNotUseMachineIfInitFails) { + this.doNotUseMachineIfInitFails = doNotUseMachineIfInitFails; + } @Override @SuppressWarnings("unchecked") @@ -351,97 +392,36 @@ public class AzureSlaveTemplate implements Describable { return labelDataSet; } - public String provisionSlaves(final TaskListener listener, int numberOfSlaves) throws Exception { - // TODO: Get nodes with label and see if we can use existing slave - return AzureManagementServiceDelegate.deployment(this, numberOfSlaves); + /** + * Provision new slaves using this template. + * @param listener + * @param numberOfSlaves Number of slaves to provision + * @return New deployment info if the provisioning was successful. + * @throws Exception May throw if provisioning was not successful. + */ + public AzureDeploymentInfo provisionSlaves(final TaskListener listener, int numberOfSlaves) throws Exception { + return AzureManagementServiceDelegate.createDeployment(this, numberOfSlaves); } - - public void waitForReadyRole(final AzureSlave slave) throws Exception { - Callable task = new Callable() { - - @Override - public Void call() throws Exception { - String status = "NA"; - while (!status.equalsIgnoreCase(Constants.READY_ROLE_STATUS)) { - LOGGER.log(Level.INFO, - "AzureSlaveTemplate: waitForReadyRole: Current status of virtual machine {0} is {1}", - new Object[] { slave.getNodeName(), status }); - Thread.sleep(30 * 1000); - status = AzureManagementServiceDelegate.getVirtualMachineStatus( - ServiceDelegateHelper.getConfiguration(azureCloud), slave.getNodeName()); - LOGGER.info("AzureSlaveTemplate: waitForReadyRole: " - + "Waiting for 30 more seconds for role to be provisioned"); - } - return null; - } - }; - - try { - ExecutionEngine.executeWithRetry(task, - new DefaultRetryStrategy(10 /* max retries */, 10 /* Default - * backoff */, - 45 * 60 /* Max. timeout in seconds */)); - LOGGER.log(Level.INFO, - "AzureSlaveTemplate: waitForReadyRole: virtual machine {0} is in ready state", slave.getNodeName()); - } catch (AzureCloudException exception) { - handleTemplateStatus("Got exception while checking for role availability " + exception, - FailureStage.PROVISIONING, slave); - LOGGER.log(Level.INFO, - "AzureSlaveTemplate: waitForReadyRole: Got exception while checking for role availability", - exception); - throw exception; - } - } - - public void handleTemplateStatus(final String message, final FailureStage failureStep, final AzureSlave slave) { - // Delete slave in azure - if (slave != null) { - Callable task = new Callable() { - - @Override - public Void call() throws Exception { - AzureManagementServiceDelegate.terminateVirtualMachine(slave, false); - return null; - } - }; - - try { - ExecutionEngine.executeWithRetry(task, new LinearRetryForAllExceptions( - 3, // maxRetries - 30, // waitinterval - 2 * 60 // timeout - )); - } catch (AzureCloudException e) { - LOGGER.log(Level.INFO, "AzureSlaveTemplate: handleTemplateStatus: could not terminate or shutdown {0}", - slave.getNodeName()); - } - } - - // Disable template if applicable - if (!templateStatus.equals(Constants.TEMPLATE_STATUS_ACTIVE_ALWAYS)) { - setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED); - // Register template for periodic check so that jenkins can make template active if validation errors - // are corrected - AzureTemplateMonitorTask.registerTemplate(this); - } else { - // Wait for a while before retry - // Failure might be during Provisioning or post provisioning. back off for 5 minutes before retry. - LOGGER.log(Level.INFO, - "AzureSlaveTemplate: handleTemplateStatus: Got {0} error, waiting for 5 minutes before retry", - failureStep); - try { - Thread.sleep(5 * 60 * 1000); - } catch (InterruptedException e) { - } - } + + /** + * If provisioning failed, handle the status and queue the template for verification. + * @param message Failure message + * @param failureStep Stage that failure occurred + */ + public void handleTemplateProvisioningFailure(final String message, final FailureStage failureStep) { + // The template is bad. It should have already been verified, but + // perhaps something changed (VHD gone, etc.). Queue for verification. + setTemplateVerified(false); + AzureVerificationTask.registerTemplate(this); + // Set the details so that it's easier to see what's going on from the configuration UI. setTemplateStatusDetails(message); } - public int getVirtualMachineCount() throws Exception { - return AzureManagementServiceDelegate.getVirtualMachineCount( - ServiceDelegateHelper.getComputeManagementClient(ServiceDelegateHelper.getConfiguration(azureCloud))); - } - + /** + * Verify that this template is correct and can be allocated. + * @return Empty list if this template is valid, list of errors otherwise + * @throws Exception + */ public List verifyTemplate() throws Exception { return AzureManagementServiceDelegate.verifyTemplate( azureCloud.getSubscriptionId(), @@ -449,7 +429,6 @@ public class AzureSlaveTemplate implements Describable { azureCloud.getClientSecret(), azureCloud.getOauth2TokenEndpoint(), azureCloud.getServiceManagementURL(), - azureCloud.getMaxVirtualMachinesLimit() + "", templateName, labels, location, @@ -469,8 +448,8 @@ public class AzureSlaveTemplate implements Describable { virtualNetworkName, subnetName, retentionTimeInMin + "", - templateStatus, jvmOptions, + getResourceGroupName(), true); } @@ -501,8 +480,8 @@ public class AzureSlaveTemplate implements Describable { public ListBoxModel doFillOsTypeItems() throws IOException, ServletException { ListBoxModel model = new ListBoxModel(); - model.add("Linux"); - model.add("Windows"); + model.add(Constants.OS_TYPE_LINUX); + model.add(Constants.OS_TYPE_WINDOWS); return model; } @@ -511,9 +490,13 @@ public class AzureSlaveTemplate implements Describable { throws IOException, ServletException { ListBoxModel model = new ListBoxModel(); - List locations = AzureManagementServiceDelegate.getVirtualMachineLocations(serviceManagementURL); + Map locations = AzureManagementServiceDelegate.getVirtualMachineLocations(serviceManagementURL); - for (String location : locations) { + // This map contains display name -> actual location name. We + // need the actual location name later, but just grab the keys of + // the map for the model. + + for (String location : locations.keySet()) { model.add(location); } @@ -528,14 +511,6 @@ public class AzureSlaveTemplate implements Describable { return model; } - public ListBoxModel doFillTemplateStatusItems() { - ListBoxModel model = new ListBoxModel(); - model.add(Constants.TEMPLATE_STATUS_ACTIVE); - model.add(Constants.TEMPLATE_STATUS_ACTIVE_ALWAYS); - model.add(Constants.TEMPLATE_STATUS_DISBALED); - return model; - } - public FormValidation doCheckInitScript( @QueryParameter final String value, @QueryParameter final String slaveLaunchMethod) { @@ -559,11 +534,40 @@ public class AzureSlaveTemplate implements Describable { return FormValidation.ok(); } + /** + * Check the template's name. Name must conform to restrictions on VM + * naming + * @param value Current name + * @param templateDisabled Is the template disabled + * @param osType OS type + * @return + */ public FormValidation doCheckTemplateName( - @QueryParameter final String value, @QueryParameter final String templateStatus) { - if (templateStatus.equals(Constants.TEMPLATE_STATUS_DISBALED)) { - return FormValidation.error(Messages.Azure_GC_TemplateStatus_Warn_Msg()); + @QueryParameter final String value, @QueryParameter final boolean templateDisabled, + @QueryParameter final String osType) { + List errors = new ArrayList(); + // Check whether the template name is valid, and then check + // whether it would be shortened on VM creation. + if (!AzureUtil.isValidTemplateName(value)) { + errors.add(FormValidation.error(Messages.Azure_GC_Template_Name_Not_Valid())); } + else { + // Check whether it would be shortened. We could just append characters, + // in which case don't error. + String shortenedName = AzureUtil.getVMBaseName(value, osType, 1); + if (!shortenedName.startsWith(value)) { + errors.add(FormValidation.warning(Messages.Azure_GC_Template_Name_Shortened(shortenedName))); + } + } + + if (templateDisabled) { + errors.add(FormValidation.warning(Messages.Azure_GC_TemplateStatus_Warn_Msg())); + } + + if (errors.size() > 0) { + return FormValidation.aggregate(errors); + } + return FormValidation.ok(); } @@ -632,7 +636,7 @@ public class AzureSlaveTemplate implements Describable { @RelativePath("..") @QueryParameter String clientSecret, @RelativePath("..") @QueryParameter String oauth2TokenEndpoint, @RelativePath("..") @QueryParameter String serviceManagementURL, - @RelativePath("..") @QueryParameter String maxVirtualMachinesLimit, + @RelativePath("..") @QueryParameter String resourceGroupName, @QueryParameter String templateName, @QueryParameter String labels, @QueryParameter String location, @@ -652,7 +656,6 @@ public class AzureSlaveTemplate implements Describable { @QueryParameter String virtualNetworkName, @QueryParameter String subnetName, @QueryParameter String retentionTimeInMin, - @QueryParameter String templateStatus, @QueryParameter String jvmOptions) { LOGGER.log(Level.INFO, @@ -662,7 +665,7 @@ public class AzureSlaveTemplate implements Describable { + "clientSecret: {2};\n\t" + "oauth2TokenEndpoint: {3};\n\t" + "serviceManagementURL: {4};\n\t" - + "maxVirtualMachinesLimit: {5};\n\t" + + "resourceGroupName: {5};\n\t." + "templateName: {6};\n\t" + "labels: {7};\n\t" + "location: {8};\n\t" @@ -682,15 +685,14 @@ public class AzureSlaveTemplate implements Describable { + "virtualNetworkName: {22};\n\t" + "subnetName: {23};\n\t" + "retentionTimeInMin: {24};\n\t" - + "templateStatus: {25};\n\t" - + "jvmOptions: {26}.", + + "jvmOptions: {25};", new Object[] { subscriptionId, clientId, (StringUtils.isNotBlank(clientSecret) ? "********" : null), oauth2TokenEndpoint, serviceManagementURL, - maxVirtualMachinesLimit, + resourceGroupName, templateName, labels, location, @@ -710,16 +712,23 @@ public class AzureSlaveTemplate implements Describable { virtualNetworkName, subnetName, retentionTimeInMin, - templateStatus, - jvmOptions }); + jvmOptions}); + // First validate the subscription info. If it is not correct, + // then we can't validate the + + String result = AzureManagementServiceDelegate.verifyConfiguration( + subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL, resourceGroupName); + if (!result.equals(Constants.OP_SUCCESS)) { + return FormValidation.error(result); + } + final List errors = AzureManagementServiceDelegate.verifyTemplate( subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL, - maxVirtualMachinesLimit, templateName, labels, location, @@ -739,8 +748,8 @@ public class AzureSlaveTemplate implements Describable { virtualNetworkName, subnetName, retentionTimeInMin, - templateStatus, jvmOptions, + resourceGroupName, false); if (errors.size() > 0) { @@ -758,7 +767,7 @@ public class AzureSlaveTemplate implements Describable { } public String getDefaultNoOfExecutors() { - return 1 + ""; + return "1"; } } diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureTemplateMonitorTask.java b/src/main/java/com/microsoftopentechnologies/azure/AzureTemplateMonitorTask.java deleted file mode 100644 index e6393b0..0000000 --- a/src/main/java/com/microsoftopentechnologies/azure/AzureTemplateMonitorTask.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - 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.HashMap; -import java.util.Map; -import java.util.logging.Logger; - -import hudson.Extension; -import hudson.model.AsyncPeriodicWork; -import hudson.model.TaskListener; -import com.microsoftopentechnologies.azure.util.Constants; -import java.util.logging.Level; -import jenkins.model.Jenkins; - -@Extension -public final class AzureTemplateMonitorTask extends AsyncPeriodicWork { - - private static final Logger LOGGER = Logger.getLogger(AzureTemplateMonitorTask.class.getName()); - - private static Map templates; - - public AzureTemplateMonitorTask() { - super("AzureTemplateMonitorTask"); - } - - @Override - public void execute(final TaskListener arg0) throws IOException, InterruptedException { - if (templates != null && !templates.isEmpty()) { - LOGGER.log(Level.INFO, "AzureTemplateMonitorTask: execute: start , template size {0}", templates.size()); - for (Map.Entry entry : templates.entrySet()) { - AzureSlaveTemplate slaveTemplate = getCloud(entry.getValue()).getAzureSlaveTemplate(entry.getKey()); - - if (slaveTemplate.getTemplateStatus().equals(Constants.TEMPLATE_STATUS_DISBALED)) { - try { - if (slaveTemplate.verifyTemplate().isEmpty()) { - // Template errors are now gone, set template to Active - slaveTemplate.setTemplateStatus(Constants.TEMPLATE_STATUS_ACTIVE); - slaveTemplate.setTemplateStatusDetails(""); - // remove from the list - templates.remove(slaveTemplate.getTemplateName()); - } - } catch (Exception e) { - // just ignore - } - } - } - LOGGER.info("AzureTemplateMonitorTask: execute: end"); - } - } - - public synchronized static void registerTemplate(final AzureSlaveTemplate template) { - if (templates == null) { - templates = new HashMap(); - } - templates.put( - template.getTemplateName(), - Constants.AZURE_CLOUD_PREFIX + template.getAzureCloud().getSubscriptionId()); - } - - @Override - public long getRecurrencePeriod() { - // Every 10 minutes - return 15 * 60 * 1000; - } - - public AzureCloud getCloud(final String cloudName) { - return Jenkins.getInstance() == null ? null : (AzureCloud) Jenkins.getInstance().getCloud(cloudName); - } -} diff --git a/src/main/java/com/microsoftopentechnologies/azure/AzureVerificationTask.java b/src/main/java/com/microsoftopentechnologies/azure/AzureVerificationTask.java new file mode 100644 index 0000000..e7b58f6 --- /dev/null +++ b/src/main/java/com/microsoftopentechnologies/azure/AzureVerificationTask.java @@ -0,0 +1,312 @@ +/* + 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 com.microsoft.windowsazure.Configuration; +import com.microsoftopentechnologies.azure.util.AzureUtil; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import hudson.Extension; +import hudson.model.AsyncPeriodicWork; +import hudson.model.TaskListener; +import com.microsoftopentechnologies.azure.util.Constants; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import jenkins.model.Jenkins; + +/** + * Performs a few types of verification: + * 1. Overall subscription verification. + * 2. Approximate VM count verification + * 3. Template verification. + * + * When a new AzureCloud is constructed or a new template is added via CLI interface, then we will + * manually trigger this workload. + * + * This thread serves as a gate for whether we can create VMs from a certain template + * @author mmitche + */ +@Extension +public final class AzureVerificationTask extends AsyncPeriodicWork { + + private static final Logger LOGGER = Logger.getLogger(AzureVerificationTask.class.getName()); + + // Templates that need verification + private static Map cloudTemplates; + + // Set of clouds that need verification. + private static Set cloudNames; + + public AzureVerificationTask() { + super("AzureVerificationTask"); + } + + private static final Object cloudNamesLock = new Object(); + private static final Object templatesLock = new Object(); + + @Override + public void execute(final TaskListener arg0) throws IOException, InterruptedException { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: start"); + + if (cloudNames == null || cloudNames.isEmpty()) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: No clouds, exiting"); + return; + } + + // Walk the list of clouds and verify the configuration. If an element + // is not found (perhaps a removed cloud, removes from the list) + synchronized (cloudNamesLock) { + List toRemove = new ArrayList(); + for (String cloudName : cloudNames) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: verifying cloud {0}", cloudName); + + AzureCloud cloud = getCloud(cloudName); + + // Unknown cloud. Maybe the name changed since the cloud name + // was registered. Remove from the list + if (cloud == null) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: subscription {0} not found, skipping", cloudName); + // Remove + toRemove.add(cloudName); + continue; + } + + // If already verified, skip + if (cloud.isConfigurationValid()) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: subscription {0} already verifed", cloudName); + // Update the count. + cloud.setVirtualMachineCount(getVirtualMachineCount(cloud)); + continue; + } + + // Verify. Update the VM count before setting to valid + if (verifyConfiguration(cloud)) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: {0} verified", cloudName); + // Update the count + cloud.setVirtualMachineCount(getVirtualMachineCount(cloud)); + // We grab the current VM count and + cloud.setConfigurationValid(true); + continue; + } + + // Not valid! Remains in list. + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: {0} not verified, has errors", cloudName); + } + + // Remove items as necessary + for (String cloudName : toRemove) { + cloudNames.remove(cloudName); + } + } + + if (cloudTemplates == null || cloudTemplates.isEmpty()) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: No templates to verify, exiting"); + return; + } + + // Now walk the templates and verify. + // Unlike the clouds, verified templates are removed from the list upon + // verification (or left if they do not verify) + synchronized (templatesLock) { + List toRemove = new ArrayList(); + + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: verifying {0} template(s)", cloudTemplates.size()); + for (Map.Entry entry : cloudTemplates.entrySet()) { + String templateName = entry.getKey(); + String cloudName = entry.getValue(); + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: verifying {0} in {1}", + new Object[] { templateName, cloudName }); + + AzureCloud cloud = getCloud(cloudName); + // If the cloud is null, could mean that the cloud details changed + // between the last time we ran this task + if (cloud == null) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: parent cloud not found for {0} in {1}", + new Object[] { templateName, cloudName }); + toRemove.add(templateName); + continue; + } + + AzureSlaveTemplate slaveTemplate = cloud.getAzureSlaveTemplate(templateName); + // Template could have been removed since the last time we ran verification + if (slaveTemplate == null) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: could not retrieve slave template named {0} in {1}", + new Object[] { templateName, cloudName }); + toRemove.add(templateName); + continue; + } + + // Determine whether we need to verify the template + if (slaveTemplate.isTemplateVerified()) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: template {0} in {1} already verified", + new Object[] { templateName, cloudName }); + // Good to go, nothing more to check here. Add to removal list. + toRemove.add(templateName); + continue; + } + // The template is not yet verified. Do so now + try { + List errors = slaveTemplate.verifyTemplate(); + if (errors.isEmpty()) { + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: {0} verified succesfully", templateName); + // Verified, set the template to verified. + slaveTemplate.setTemplateVerified(true); + // Reset the status details + slaveTemplate.setTemplateStatusDetails(""); + } + else { + String details = String.join("\n", errors); + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: {0} could not be verified:\n{1}", + new Object [] { templateName, details }); + // Set the status details to the set of messages + slaveTemplate.setTemplateStatusDetails(details); + } + } + catch (Exception e) { + // Log, but ignore overall + LOGGER.log(Level.INFO, "AzureVerificationTask: execute: got exception while verifying {0}:\n{1}", + new Object [] { templateName, e.toString() }); + } + } + + // Remove items as necessary + for (String templateName : toRemove) { + cloudTemplates.remove(templateName); + } + } + + LOGGER.info("AzureVerificationTask: execute: end"); + } + + /** + * Checks the subscription for validity if needed + * @param cloud + * @return True if the subscription is valid, false otherwise. + * Updates the cloud state if it is. If subscription is + * not valid, then we can just return + */ + public boolean verifyConfiguration(AzureCloud cloud) { + LOGGER.info("AzureVerificationTask: verifyConfiguration: start"); + + // Check the sub and off we go + String result = AzureManagementServiceDelegate.verifyConfiguration(cloud.getSubscriptionId(), + cloud.getClientId(), cloud.getClientSecret(), cloud.getOauth2TokenEndpoint(), cloud.getServiceManagementURL(), cloud.getResourceGroupName()); + if (result != Constants.OP_SUCCESS) { + LOGGER.log(Level.INFO, "AzureVerificationTask: verifyConfiguration: {0}", result); + cloud.setConfigurationValid(false); + return false; + } + + return true; + } + + /** + * Retrieve the current VM count. + * @param cloud + * @return + */ + public int getVirtualMachineCount(AzureCloud cloud) { + LOGGER.info("AzureVerificationTask: getVirtualMachineCount: start"); + try { + Configuration config = ServiceDelegateHelper.getConfiguration(cloud); + int vmCount = AzureManagementServiceDelegate.getVirtualMachineCount(config); + LOGGER.log(Level.INFO, "AzureVerificationTask: getVirtualMachineCount: end, currently {0} vms", vmCount); + return vmCount; + } + catch(Exception e) { + LOGGER.log(Level.INFO, "AzureVerificationTask: getVirtualMachineCount: failed to retrieve vm count:\n{0}", + e.toString()); + // We could have failed for any number of reasons. Just return the current + // number of virtual machines. + return cloud.getApproximateVirtualMachineCount(); + } + } + + /** + * Register more than one template at once + * @param templatesToRegister List of templates to register + */ + public static void registerTemplates(final List templatesToRegister) { + synchronized(templatesLock) { + for (AzureSlaveTemplate template : templatesToRegister) { + registerTemplateHelper(template); + } + } + } + + /** + * Registers a template for verification + * @param template Template to register + */ + public static void registerTemplate(final AzureSlaveTemplate template) { + synchronized(templatesLock) { + registerTemplateHelper(template); + } + } + + /** + * Registers a single template. The lock should be held while calling this method + * @param template Template to register + */ + private static void registerTemplateHelper(final AzureSlaveTemplate template) { + String cloudName = AzureUtil.getCloudName(template.getAzureCloud().getSubscriptionId()); + LOGGER.log(Level.INFO, "AzureVerificationTask: registerTemplateHelper: Registering template {0} on {1} for verification", + new Object [] { template.getTemplateName(), cloudName }); + if (cloudTemplates == null) { + cloudTemplates = new HashMap(); + } + cloudTemplates.put(template.getTemplateName(), cloudName); + } + + /** + * Register a cloud for verification + * @param cloudName + */ + public static void registerCloud(final String cloudName) { + LOGGER.log(Level.INFO, "AzureVerificationTask: registerCloud: Registering cloud {0} for verification", + cloudName); + synchronized(cloudNamesLock) { + if (cloudNames == null) { + cloudNames = new HashSet(); + } + cloudNames.add(cloudName); + } + } + + @Override + public long getRecurrencePeriod() { + // Every 5 minutes + return 5 * 60 * 1000; + } + + public AzureCloud getCloud(final String cloudName) { + return Jenkins.getInstance() == null ? null : (AzureCloud) Jenkins.getInstance().getCloud(cloudName); + } + + /** + * Retrieve the verification task worker. Can be used to force work + * @return The AzureVerificationTask worker class + */ + public static AzureVerificationTask get() { + return AsyncPeriodicWork.all().get(AzureVerificationTask.class); + } +} diff --git a/src/main/java/com/microsoftopentechnologies/azure/ServiceDelegateHelper.java b/src/main/java/com/microsoftopentechnologies/azure/ServiceDelegateHelper.java index 9ebc41e..6a35395 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/ServiceDelegateHelper.java +++ b/src/main/java/com/microsoftopentechnologies/azure/ServiceDelegateHelper.java @@ -135,9 +135,6 @@ public class ServiceDelegateHelper { subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL).get(). getConfiguration(); - LOGGER.log(Level.INFO, "Configuration token: {0}", TokenCloudCredentials.class.cast( - config.getProperty(SUBSCRIPTION_CLOUD_CREDENTIALS)).getToken()); - return config; } finally { Thread.currentThread().setContextClassLoader(thread); diff --git a/src/main/java/com/microsoftopentechnologies/azure/StorageServiceDelegate.java b/src/main/java/com/microsoftopentechnologies/azure/StorageServiceDelegate.java index 9e5e1c8..31ea362 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/StorageServiceDelegate.java +++ b/src/main/java/com/microsoftopentechnologies/azure/StorageServiceDelegate.java @@ -66,14 +66,15 @@ public class StorageServiceDelegate { * @return list of storage account URIs * @throws Exception */ - private static List getStorageAccountURIs(final Configuration config, final String storageAccountName) throws + private static List getStorageAccountURIs(final Configuration config, final String storageAccountName, + final String resourceGroupName) throws AzureCloudException { StorageManagementClient client = StorageManagementService.create(config); StorageAccountGetPropertiesResponse response; try { response = client.getStorageAccountsOperations(). - getProperties(Constants.RESOURCE_GROUP_NAME, storageAccountName); + getProperties(resourceGroupName, storageAccountName); } catch (Exception e) { throw new AzureCloudException("StorageServiceDelegate: getStorageAccountURIs: storage account with name " + storageAccountName + " does not exist"); @@ -116,13 +117,13 @@ public class StorageServiceDelegate { * @throws AzureCloudException */ public static StorageAccountGetPropertiesResponse getStorageAccountProps( - final Configuration config, final String storageAccountName) + final Configuration config, final String storageAccountName, final String resourceGroupName) throws AzureCloudException { StorageManagementClient client = StorageManagementService.create(config); StorageAccountGetPropertiesResponse response = null; try { response = client.getStorageAccountsOperations().getProperties( - Constants.RESOURCE_GROUP_NAME, storageAccountName); + resourceGroupName, storageAccountName); } catch (Exception e) { throw new AzureCloudException("StorageServiceDelegate: getStorageAccountURIs: storage account with name " + storageAccountName + " does not exist"); @@ -141,14 +142,15 @@ public class StorageServiceDelegate { * @return * @throws AzureCloudException */ - public static String getStorageAccountURI(Configuration config, String storageAccountName, String type) throws + public static String getStorageAccountURI(Configuration config, String storageAccountName, String type, + String resourceGroupName) throws AzureCloudException { String serviceURI = null; String defaultURL = Constants.HTTP_PROTOCOL_PREFIX + storageAccountName + DOT_CHAR + type + Constants.BASE_URI_SUFFIX; // Get service URLS - List storageAccountURLs = getStorageAccountURIs(config, storageAccountName); + List storageAccountURLs = getStorageAccountURIs(config, storageAccountName, resourceGroupName); if (storageAccountURLs == null || storageAccountURLs.isEmpty()) { LOGGER.info("StorageServiceDelegate: getStorageAccountURI: storageAccountURLs is null, returning default"); @@ -181,32 +183,32 @@ public class StorageServiceDelegate { * @param storageAccountName Azure storage account name * @param key Azure storage account key * @param baseURI Azure storage account blob url. - * @param fileName blob file name - * @param initScript contents of file to be uploaded + * @param blobName blob file name + * @param fileContents contents of file to be uploaded * @return blob url * @throws AzureCloudException * @throws URISyntaxException * @throws StorageException * @throws IOException */ - public static String uploadConfigFileToStorage(Configuration config, String storageAccountName, String key, - String baseURI, - String fileName, String initScript) throws AzureCloudException, URISyntaxException, StorageException, + public static String uploadFileToStorage(Configuration config, String storageAccountName, String key, + String baseURI, String resourceGroupName, String containerName, + String blobName, byte[] fileContents) throws AzureCloudException, URISyntaxException, StorageException, IOException { CloudBlockBlob blob = null; - String storageURI = getStorageAccountURI(config, storageAccountName, Constants.BLOB); + String storageURI = getStorageAccountURI(config, storageAccountName, Constants.BLOB, resourceGroupName); CloudBlobContainer container = getBlobContainerReference(storageAccountName, key, storageURI, - Constants.CONFIG_CONTAINER_NAME); - blob = container.getBlockBlobReference(fileName); - InputStream is = new ByteArrayInputStream(initScript.getBytes("UTF-8")); + containerName); + blob = container.getBlockBlobReference(blobName); + InputStream is = new ByteArrayInputStream(fileContents); try { - blob.upload(is, initScript.length()); + blob.upload(is, fileContents.length); } finally { is.close(); } - return storageURI + Constants.CONFIG_CONTAINER_NAME + "/" + fileName; + return storageURI + Constants.CONFIG_CONTAINER_NAME + "/" + blobName; } /** @@ -220,7 +222,7 @@ public class StorageServiceDelegate { * @throws URISyntaxException * @throws StorageException */ - private static CloudBlobContainer getBlobContainerReference(String storageAccountName, String key, String blobURL, + public static CloudBlobContainer getBlobContainerReference(String storageAccountName, String key, String blobURL, String containerName) throws URISyntaxException, StorageException { CloudStorageAccount cloudStorageAccount; CloudBlobClient serviceClient; @@ -247,7 +249,6 @@ public class StorageServiceDelegate { } else { return null; } - } /** diff --git a/src/main/java/com/microsoftopentechnologies/azure/remote/AzureSSHLauncher.java b/src/main/java/com/microsoftopentechnologies/azure/remote/AzureSSHLauncher.java index 2546a1c..7d0f358 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/remote/AzureSSHLauncher.java +++ b/src/main/java/com/microsoftopentechnologies/azure/remote/AzureSSHLauncher.java @@ -20,10 +20,14 @@ import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; +import com.microsoftopentechnologies.azure.AzureCloud; import com.microsoftopentechnologies.azure.AzureSlave; import com.microsoftopentechnologies.azure.AzureComputer; +import com.microsoftopentechnologies.azure.AzureSlaveTemplate; import com.microsoftopentechnologies.azure.Messages; +import com.microsoftopentechnologies.azure.util.CleanUpAction; import com.microsoftopentechnologies.azure.util.Constants; +import com.microsoftopentechnologies.azure.util.FailureStage; import hudson.model.Descriptor; import hudson.model.TaskListener; @@ -36,6 +40,7 @@ import hudson.slaves.SlaveComputer; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintStream; import java.net.ConnectException; import java.net.UnknownHostException; @@ -45,6 +50,7 @@ import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.jvnet.localizer.Localizable; /** * SSH Launcher class @@ -60,19 +66,25 @@ public class AzureSSHLauncher extends ComputerLauncher { @Override public void launch(final SlaveComputer slaveComputer, final TaskListener listener) { - LOGGER.info("AzureSSHLauncher: launch: launch method called for slave "); AzureComputer computer = (AzureComputer) slaveComputer; AzureSlave slave = computer.getNode(); + + LOGGER.log(Level.INFO,"AzureSSHLauncher: launch: launch method called for slave {0}", slaveComputer.getName()); - //check if VM is already stopped or stopping or getting deleted , if yes then there is no point in trying to connect - //Added this check - since after restarting jenkins master, jenkins is trying to connect to all the slaves although slaves are suspended. + // Check if VM is already stopped or stopping or getting deleted , if yes then there is no point in trying to connect + // Added this check - since after restarting jenkins master, jenkins is trying to connect to all the slaves although slaves are suspended. + // This still means that a delete slave will eventually get cleaned up. try { if (!slave.isVMAliveOrHealthy()) { + LOGGER.log(Level.INFO,"AzureSSHLauncher: launch: Slave {0} is shut down, deleted, etc. Not attempting to connect", slaveComputer.getName()); return; } } catch (Exception e1) { // ignoring exception purposefully } + + // Block cleanup while we attempt to start. + slave.blockCleanUpAction(); PrintStream logger = listener.getLogger(); boolean successful = false; @@ -83,28 +95,32 @@ public class AzureSSHLauncher extends ComputerLauncher { } catch (UnknownHostException e) { LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: " + "Got unknown host exception. Virtual machine might have been deleted already", e); - slave.setDeleteSlave(true); } catch (ConnectException e) { LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: Got connect exception. Might be due to firewall rules", e); - markSlaveForDeletion(slave, Constants.SLAVE_POST_PROV_CONN_FAIL); + handleLaunchFailure(slave, Constants.SLAVE_POST_PROV_CONN_FAIL); } catch (Exception e) { // Checking if we need to mark template as disabled. Need to re-visit this logic based on tests. if (e.getMessage() != null && e.getMessage().equalsIgnoreCase("Auth fail")) { LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: " + "Authentication failure. Image may not be supporting password authentication", e); - markSlaveForDeletion(slave, Constants.SLAVE_POST_PROV_AUTH_FAIL); + handleLaunchFailure(slave, Constants.SLAVE_POST_PROV_AUTH_FAIL); } else { LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: Got exception", e); - markSlaveForDeletion(slave, Constants.SLAVE_POST_PROV_CONN_FAIL + e.getMessage()); + handleLaunchFailure(slave, Constants.SLAVE_POST_PROV_CONN_FAIL + e.getMessage()); + } + } + finally { + if (session == null) { + slave.getComputer().setAcceptingTasks(false); + slave.setCleanUpAction(CleanUpAction.DELETE, Messages._Slave_Failed_To_Connect()); + return; } } - if (session == null) { - return; - } - + Localizable cleanUpReason = null; + try { final Session cleanupSession = session; String initScript = slave.getInitScript(); @@ -117,10 +133,18 @@ public class AzureSSHLauncher extends ComputerLauncher { // Execute initialization script // Make sure to change file permission for execute if needed. TODO: need to test - int exitStatus = executeRemoteCommand(session, "sh " + remoteInitFileName, logger); + + String command = "sh " + remoteInitFileName; + int exitStatus = executeRemoteCommand(session, command, logger, slave.getExecuteInitScriptAsRoot(), slave.getAdminPassword()); if (exitStatus != 0) { - LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: init script failed: exit code={0}", exitStatus); - //TODO: Do we need to expose flag and act accordingly?? For now ignoring init script failures + if (slave.getDoNotUseMachineIfInitFails()) { + LOGGER.log(Level.SEVERE, "AzureSSHLauncher: launch: init script failed: exit code={0} (marking slave for deletion)", exitStatus); + cleanUpReason = Messages._Slave_Failed_Init_Script(); + return; + } + else { + LOGGER.log(Level.INFO, "AzureSSHLauncher: launch: init script failed: exit code={0} (ignoring)", exitStatus); + } } else { LOGGER.info("AzureSSHLauncher: launch: init script got executed successfully"); } @@ -133,7 +157,7 @@ public class AzureSSHLauncher extends ComputerLauncher { if (executeRemoteCommand(session, "java -fullversion", logger) != 0) { LOGGER.info("AzureSSHLauncher: launch: Java not found. " + "At a minimum init script should ensure that java runtime is installed"); - markSlaveForDeletion(slave, Constants.SLAVE_POST_PROV_JAVA_NOT_FOUND); + handleLaunchFailure(slave, Constants.SLAVE_POST_PROV_JAVA_NOT_FOUND); return; } @@ -166,6 +190,10 @@ public class AzureSSHLauncher extends ComputerLauncher { }); LOGGER.info("AzureSSHLauncher: launch: launched slave successfully"); + // There's a chance that it was marked as delete (for instance, if the node + // was unreachable and then someone hit connect and it worked. Reset the node cleanup + // state to the default for the node. + slave.clearCleanUpAction(); successful = true; } catch (Exception e) { LOGGER.log(Level.INFO, "AzureSSHLauncher: launch: got exception ", e); @@ -174,6 +202,12 @@ public class AzureSSHLauncher extends ComputerLauncher { if (session != null) { session.disconnect(); } + if (cleanUpReason == null) { + cleanUpReason = Messages._Slave_Failed_To_Connect(); + } + slave.getComputer().setAcceptingTasks(false); + // Set the machine to be deleted by the cleanup task + slave.setCleanUpAction(CleanUpAction.DELETE, cleanUpReason); } } } @@ -200,6 +234,8 @@ public class AzureSSHLauncher extends ComputerLauncher { new Object[] { dnsName, sshPort, e.getMessage() }); throw e; } + + } private void copyFileToRemote(Session jschSession, InputStream stream, String remotePath) throws Exception { @@ -236,19 +272,55 @@ public class AzureSSHLauncher extends ComputerLauncher { } } } - + + /** + * Helper method for most common call (without root) + * @param jschSession + * @param command + * @param logger + * @return + */ private int executeRemoteCommand(final Session jschSession, final String command, final PrintStream logger) { + return executeRemoteCommand(jschSession, command, logger, false, null); + } + + /** + * Executes a remote command, as root if desired + * @param jschSession + * @param command + * @param logger + * @param executeAsRoot + * @param passwordIfRoot + * @return + */ + private int executeRemoteCommand(final Session jschSession, final String command, final PrintStream logger, boolean executeAsRoot, String passwordIfRoot) { ChannelExec channel = null; - LOGGER.info("AzureSSHLauncher: executeRemoteCommand: starts"); try { + // If root, modify the command to set up sudo -S + String finalCommand = null; + if (executeAsRoot) { + finalCommand = "sudo -S -p '' " + command; + } + else { + finalCommand = command; + } + LOGGER.log(Level.INFO, "AzureSSHLauncher: executeRemoteCommand: starting {0}", command); + channel = (ChannelExec) jschSession.openChannel("exec"); - channel.setCommand(command); + channel.setCommand(finalCommand); channel.setInputStream(null); channel.setErrStream(System.err); final InputStream inputStream = channel.getInputStream(); final InputStream errorStream = channel.getErrStream(); + final OutputStream outputStream = channel.getOutputStream(); channel.connect(60 * 1000); + // If as root, push the password + if (executeAsRoot) { + outputStream.write((passwordIfRoot + "\n").getBytes()); + outputStream.flush(); + } + // Read from input stream try { IOUtils.copy(inputStream, logger); @@ -274,7 +346,7 @@ public class AzureSSHLauncher extends ComputerLauncher { } } - LOGGER.info("AzureSSHLauncher: executeRemoteCommand: ends successfully"); + LOGGER.info("AzureSSHLauncher: executeRemoteCommand: executed successfully"); return channel.getExitStatus(); } catch (JSchException jse) { LOGGER.log(Level.SEVERE, @@ -322,12 +394,20 @@ public class AzureSSHLauncher extends ComputerLauncher { } } - private static void markSlaveForDeletion(AzureSlave slave, String message) { - slave.setTemplateStatus(Constants.TEMPLATE_STATUS_DISBALED, message); - if (slave.toComputer() != null) { - slave.toComputer().setTemporarilyOffline(true, OfflineCause.create(Messages._Slave_Failed_To_Connect())); + /** + * Mark the slave for deletion and queue the corresponding template for verification + * @param slave + * @param message + */ + private void handleLaunchFailure(AzureSlave slave, String message) { + // Queue the template for verification in case something happened there. + AzureCloud azureCloud = slave.getCloud(); + if (azureCloud != null) { + AzureSlaveTemplate slaveTemplate = azureCloud.getAzureSlaveTemplate(slave.getTemplateName()); + if (slaveTemplate != null) { + slaveTemplate.handleTemplateProvisioningFailure(message, FailureStage.POSTPROVISIONING); + } } - slave.setDeleteSlave(true); } @Override diff --git a/src/main/java/com/microsoftopentechnologies/azure/util/AccessToken.java b/src/main/java/com/microsoftopentechnologies/azure/util/AccessToken.java index dbb3392..3aa45e6 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/util/AccessToken.java +++ b/src/main/java/com/microsoftopentechnologies/azure/util/AccessToken.java @@ -42,8 +42,8 @@ public class AccessToken implements Serializable { this.subscriptionId = subscriptionId; this.serviceManagementUrl = serviceManagementUrl; this.token = authres.getAccessToken(); - // In the 0.9.4 version of the SDK, expiresOn is the number of ms till expire - this.expiration = System.currentTimeMillis() + authres.getExpiresOn(); + // In the 0.9.4 version of the SDK, expiresOn is the number of seconds till expire + this.expiration = System.currentTimeMillis() + authres.getExpiresOn() * 1000; } public Configuration getConfiguration() throws AzureCloudException { diff --git a/src/main/java/com/microsoftopentechnologies/azure/util/AzureUtil.java b/src/main/java/com/microsoftopentechnologies/azure/util/AzureUtil.java index 99ee5d9..dc1b25a 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/util/AzureUtil.java +++ b/src/main/java/com/microsoftopentechnologies/azure/util/AzureUtil.java @@ -15,6 +15,9 @@ */ package com.microsoftopentechnologies.azure.util; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Date; import org.apache.commons.lang.StringUtils; public class AzureUtil { @@ -38,6 +41,8 @@ public class AzureUtil { public static final String VAL_PASSWORD_REGEX = "([0-9a-zA-Z!@#\\$%\\^&\\*\\.]*{8,123})"; public static final String VAL_ADMIN_USERNAME = "([a-zA-Z0-9_-]{3,15})"; + + public static final String VAL_TEMPLATE = "^[a-z][a-z0-9-]*[a-z0-9]$"; // Although ugly to maintain this is best way for now. public static String DEFAULT_INIT_SCRIPT = "Set-ExecutionPolicy Unrestricted" + "\n" @@ -262,4 +267,109 @@ public class AzureUtil { return errorMessage.contains("The specified deployment slot Production is occupied"); } + + /** + * Retrieves the name of the cloud for registering with Jenkins + * @param subscriptionId Subscription id + * @return Name of the cloud + */ + public static String getCloudName(String subscriptionId) { + return Constants.AZURE_CLOUD_PREFIX + subscriptionId; + } + + /** + * Returns a template name that can be used for the base of a VM name + * @return A shortened template name if required, the full name otherwise + */ + private static String getShortenedTemplateName(String templateName, String usageType, int dateDigits, int extraSuffixDigits) { + // We'll be adding on 10 characters for the deployment ID (which is a formatted date) + // Plus an index of the + // The template name should already be valid at least, so check that first + if (!isValidTemplateName(templateName)) { + throw new IllegalArgumentException("Template name is not valid"); + } + // If the template name ends in a number, we add a dash + // to split up the name + + int maxLength; + if (usageType.equals(Constants.OS_TYPE_LINUX)) { + // Linux, length <= 63 characters, 10 characters for the date + maxLength = 63; + } + else if (usageType.equals(Constants.OS_TYPE_WINDOWS)) { + // Windows, length is 15 characters. 10 characters for the date + maxLength = 15; + } + else if (usageType.equals(Constants.USAGE_TYPE_DEPLOYMENT)) { + // Maximum is 64 characters + maxLength = 64; + } + else { + throw new IllegalArgumentException("Unknown OS/Usage type"); + } + + // Chop of what we need for date digits + maxLength -= dateDigits; + // Chop off extra if needed for suffix digits + maxLength -= extraSuffixDigits; + + // Shorten the name + String shortenedName = templateName.substring(0,Math.min(templateName.length(), maxLength)); + + // If the name ends in a digit, either append or replace the last char with a - so it's + // not confusing + if (StringUtils.isNumeric(shortenedName.substring(shortenedName.length()-1))) { + shortenedName = shortenedName.substring(0, Math.min(templateName.length(), maxLength-1)); + shortenedName += '-'; + } + + return shortenedName; + } + + /** + * Returns true if the template name is valid, false otherwise + * @param templateName Template name to validate + * @return True if the template is valid, false otherwise + */ + public static boolean isValidTemplateName(String templateName) { + return templateName.matches(VAL_TEMPLATE); + } + + /** + * Creates a deployment given a template name and OS type + * @param templateName Valid template name + * @param osType Valid os type + * @return Valid deployment name to use for a new deployment + */ + public static String getDeploymentName(String templateName) { + if (!isValidTemplateName(templateName)) { + throw new IllegalArgumentException("Invalid template name"); + } + + Format formatter = new SimpleDateFormat(Constants.DEPLOYMENT_NAME_DATE_FORMAT); + return String.format("%s%s", getShortenedTemplateName(templateName, Constants.USAGE_TYPE_DEPLOYMENT, + Constants.DEPLOYMENT_NAME_DATE_FORMAT.length(), 0), + formatter.format(new Date(System.currentTimeMillis()))); + } + + /** + * Creates a new VM base name given the input parameters. + * @param templateName Template name + * @param osType Type of OS + * @param numberOfVmsToCreate Number of VMs that will be created + * (which is added to the suffix of the VM name by azure) + * @return + */ + public static String getVMBaseName(String templateName, String osType, int numberOfVMs) { + if (!isValidTemplateName(templateName)) { + throw new IllegalArgumentException("Invalid template name"); + } + + // For VM names, we use a simpler form. VM names are pretty short + Format formatter = new SimpleDateFormat(Constants.VM_NAME_DATE_FORMAT); + int numberOfDigits = (int)Math.floor(Math.log10((double)numberOfVMs))+1; + return String.format("%s%s", getShortenedTemplateName(templateName, osType, + Constants.VM_NAME_DATE_FORMAT.length(), numberOfDigits), + formatter.format(new Date(System.currentTimeMillis()))); + } } diff --git a/src/main/java/com/microsoftopentechnologies/azure/util/CleanUpAction.java b/src/main/java/com/microsoftopentechnologies/azure/util/CleanUpAction.java new file mode 100644 index 0000000..9571522 --- /dev/null +++ b/src/main/java/com/microsoftopentechnologies/azure/util/CleanUpAction.java @@ -0,0 +1,34 @@ +/* + Copyright 2016 Microsoft, 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.util; + +/** + * Represents the action that should be taken by the Azure Slave CleanUp Task + * if the machine is to be cleaned up if it is in an offline state. + * @author mmitche + */ +public enum CleanUpAction { + // Machine should be kept and not cleaned up even if in an offline state. + // In this state during creation or if machine was previously shut down. + BLOCK, + // Machine should be deleted if in an offline state + DELETE, + // Machine should be shut down if in an offline state + SHUTDOWN, + // Machine should perform the default action for the node (shutdown or delete) + DEFAULT +} diff --git a/src/main/java/com/microsoftopentechnologies/azure/util/Constants.java b/src/main/java/com/microsoftopentechnologies/azure/util/Constants.java index c3cd254..fa4dba4 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/util/Constants.java +++ b/src/main/java/com/microsoftopentechnologies/azure/util/Constants.java @@ -37,8 +37,6 @@ public class Constants { public static final String FWD_SLASH = "/"; - public static final String VM_NAME_PREFIX = "Azure"; - public static final int DEFAULT_MAX_VM_LIMIT = 10; public static final int DEFAULT_IDLE_TIME = 60; @@ -57,19 +55,19 @@ public class Constants { public static final String OS_TYPE_WINDOWS = "Windows"; public static final String OS_TYPE_LINUX = "Linux"; + + /** Usage types for template names **/ + public static final String USAGE_TYPE_DEPLOYMENT = "Deployment"; + + /** VM/Deployment name date formats **/ + public static final String VM_NAME_DATE_FORMAT = "HHmmss"; + public static final String DEPLOYMENT_NAME_DATE_FORMAT = "MMddHHmmss"; /** Slaves launch method */ public static final String LAUNCH_METHOD_JNLP = "JNLP"; public static final String LAUNCH_METHOD_SSH = "SSH"; - /** Template Status */ - public static final String TEMPLATE_STATUS_ACTIVE = "Active until first failure"; - - public static final String TEMPLATE_STATUS_ACTIVE_ALWAYS = "Active always"; - - public static final String TEMPLATE_STATUS_DISBALED = "Disabled"; - public static final int MAX_PROV_RETRIES = 20; /** Error codes */ @@ -114,18 +112,19 @@ public class Constants { public static final String REG_EX_DIGIT = "\\d+"; /** Role Status */ - public static final String READY_ROLE_STATUS = "ReadyRole"; - public static final String DELETING_VM_STATUS = "DeletingVM"; + public static final String STOPPED_VM_STATUS = "STOPPED"; - public static final String STOPPED_VM_STATUS = "StoppedVM"; + public static final String STOPPING_VM_STATUS = "STOPPING"; + + public static final String STARTING_VM_STATUS = "STARTING"; + + public static final String RUNNING_VM_STATUS = "RUNNING"; + + public static final String DEALLOCATED_VM_STATUS = "DEALLOCATED"; + + public static final String PROVISIONING_OR_DEPROVISIONING_VM_STATUS = "PROVISIONING_OR_DEPROVISIONING"; - public static final String STOPPING_VM_STATUS = "StoppingVM"; - - public static final String STOPPING_ROLE_STATUS = "StoppingRole"; - - public static final String STOPPED_DEALLOCATED_VM_STATUS = "StoppedDeallocated"; - - public static final String RESOURCE_GROUP_NAME = "jenkins"; + public static final String DEFAULT_RESOURCE_GROUP_NAME = "jenkins"; } diff --git a/src/main/java/com/microsoftopentechnologies/azure/util/TokenCache.java b/src/main/java/com/microsoftopentechnologies/azure/util/TokenCache.java index 33cca83..84fea2c 100644 --- a/src/main/java/com/microsoftopentechnologies/azure/util/TokenCache.java +++ b/src/main/java/com/microsoftopentechnologies/azure/util/TokenCache.java @@ -91,7 +91,7 @@ public class TokenCache { final String clientSecret, final String oauth2TokenEndpoint, final String serviceManagementURL) { - LOGGER.info("Instantiate new cache manager"); + LOGGER.log(Level.FINEST, "TokenCache: TokenCache: Instantiate new cache manager"); this.subscriptionId = subscriptionId; this.clientId = clientId; @@ -106,21 +106,21 @@ public class TokenCache { final String home = Jenkins.getInstance().root.getPath(); - LOGGER.log(Level.INFO, "Cache home \"{0}\"", home); + LOGGER.log(Level.FINEST, "TokenCache: TokenCache: Cache home \"{0}\"", home); final StringBuilder builder = new StringBuilder(home); builder.append(File.separatorChar).append("azuretoken.txt"); this.path = builder.toString(); - LOGGER.log(Level.INFO, "Cache file path \"{0}\"", path); + LOGGER.log(Level.FINEST, "TokenCache: TokenCache: Cache file path \"{0}\"", path); } public AccessToken get() throws AzureCloudException { - LOGGER.log(Level.INFO, "Get token from cache"); + LOGGER.log(Level.FINEST, "TokenCache: get: Get token from cache"); synchronized (tsafe) { AccessToken token = readTokenFile(); if (token == null || token.isExpiring()) { - LOGGER.log(Level.INFO, "Token is no longer valid ({0})", + LOGGER.log(Level.FINEST, "TokenCache: get: Token is no longer valid ({0})", token == null ? null : token.getExpirationDate()); clear(); token = getNewToken(); @@ -130,12 +130,12 @@ public class TokenCache { } public final void clear() { - LOGGER.log(Level.INFO, "Remove cache file {0}", path); + LOGGER.log(Level.FINEST, "TokenCache: clear: Remove cache file {0}", path); FileUtils.deleteQuietly(new File(path)); } private AccessToken readTokenFile() { - LOGGER.log(Level.INFO, "Read token from file {0}", path); + LOGGER.log(Level.FINEST, "TokenCache: readTokenFile: Read token from file {0}", path); FileInputStream is = null; ObjectInputStream objectIS = null; @@ -146,14 +146,14 @@ public class TokenCache { objectIS = new ObjectInputStream(is); return AccessToken.class.cast(objectIS.readObject()); } else { - LOGGER.log(Level.INFO, "File {0} does not exist", path); + LOGGER.log(Level.FINEST, "TokenCache: readTokenFile: File {0} does not exist", path); } } catch (FileNotFoundException e) { - LOGGER.log(Level.SEVERE, "Cache file not found", e); + LOGGER.log(Level.SEVERE, "TokenCache: readTokenFile: Cache file not found", e); } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error reading serialized object", e); + LOGGER.log(Level.SEVERE, "TokenCache: readTokenFile: Error reading serialized object", e); } catch (ClassNotFoundException e) { - LOGGER.log(Level.SEVERE, "Error deserializing object", e); + LOGGER.log(Level.SEVERE, "TokenCache: readTokenFile: Error deserializing object", e); } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(objectIS); @@ -163,7 +163,7 @@ public class TokenCache { } private boolean writeTokenFile(final AccessToken token) { - LOGGER.log(Level.INFO, "Write token into file {0}", path); + LOGGER.log(Level.FINEST, "TokenCache: writeTokenFile: Write token into file {0}", path); FileOutputStream fout = null; ObjectOutputStream oos = null; @@ -176,9 +176,9 @@ public class TokenCache { oos.writeObject(token); res = true; } catch (FileNotFoundException e) { - LOGGER.log(Level.SEVERE, "Cache file not found", e); + LOGGER.log(Level.SEVERE, "TokenCache: writeTokenFile: Cache file not found", e); } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error serializing object", e); + LOGGER.log(Level.SEVERE, "TokenCache: writeTokenFile: Error serializing object", e); } finally { IOUtils.closeQuietly(fout); IOUtils.closeQuietly(oos); @@ -188,14 +188,14 @@ public class TokenCache { } private AccessToken getNewToken() throws AzureCloudException { - LOGGER.log(Level.INFO, "Retrieve new access token"); + LOGGER.log(Level.FINEST, "TokenCache: getNewToken: Retrieve new access token"); final ExecutorService service = Executors.newFixedThreadPool(1); AuthenticationResult authres = null; try { - LOGGER.log(Level.INFO, "Aquiring access token: \n\t{0}\n\t{1}\n\t{2}", + LOGGER.log(Level.FINEST, "TokenCache: getNewToken: Aquiring access token: \n\t{0}\n\t{1}\n\t{2}", new Object[] { oauth2TokenEndpoint, serviceManagementURL, clientId }); final ClientCredential credential = new ClientCredential(clientId, clientSecret); @@ -220,10 +220,6 @@ public class TokenCache { final AccessToken token = new AccessToken(subscriptionId, serviceManagementURL, authres); - LOGGER.log(Level.INFO, - "Authentication result:\n\taccess token: {0}\n\tExpires In: {1}{2}", - new Object[] { token.getToken(), token.getExpirationDate()}); - writeTokenFile(token); return token; } diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.jelly b/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.jelly index 8cd3af7..e7f74e1 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.jelly +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.jelly @@ -25,6 +25,10 @@ + + + + @@ -33,7 +37,7 @@ + with="subscriptionId,clientId,clientSecret,oauth2TokenEndpoint,serviceManagementURL,resourceGroupName" /> diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.properties b/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.properties index cac5b0a..894542c 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.properties +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureCloud/config.properties @@ -3,9 +3,10 @@ Subscription_ID=Subscription ID Client_Id=Client ID Client_Secret=Client Secret OAuth2_Token_Endpoint=OAuth 2.0 Token Endpoint -Max_Virtual_Machines_Limit=Max Virtual Machines Limit +Max_Virtual_Machines_Limit=Max Virtual Machines Limit Service_Management_URL=Management Service URL Verify_Configuration=Verify Configuration Verifying=Verifying... Azure_Virtual_Machine_Template=Add Azure Virtual Machine Template Azure_Virtual_Machine_Template_desc=Azure instances to be provisioned as slaves +Resource_Group_Name=Resource Group Name diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.jelly b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.jelly index a5a823d..2919bff 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.jelly +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.jelly @@ -2,6 +2,7 @@ + diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.properties b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.properties index a6be7cc..55cceb6 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.properties +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlave/configure-entries.properties @@ -1 +1 @@ -Azure_Slave_Configuration=Azure_Slave_Configuration +Azure_Slave_Configuration=Azure Slave Configuration diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.jelly b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.jelly index 2cf9fa8..6354b98 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.jelly +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.jelly @@ -1,8 +1,8 @@ - - + + diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.properties b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.properties index 52011a3..d6b7bb4 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.properties +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlavePostBuildAction/config.properties @@ -1,2 +1,2 @@ PostBuild_Action_Azure_Slave=Post Build action for Azure Slave. -Failover_Action_Slave=Failover action on Azure slave(applicable for failed jobs only) \ No newline at end of file +Perform_The_Following_On_Completion=Perform the following on build completion \ No newline at end of file diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.jelly b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.jelly index 33c386c..1beb309 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.jelly +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.jelly @@ -32,7 +32,7 @@ - + @@ -42,9 +42,6 @@ - - - @@ -57,23 +54,37 @@ - + + + + + - - - - - + + + + + + + + + + + + + + + - + @@ -100,8 +111,8 @@ - - + + @@ -114,7 +125,6 @@ - + with="subscriptionId,clientId,clientSecret,oauth2TokenEndpoint,serviceManagementURL,resourceGroupName,templateName,labels,location,virtualMachineSize,storageAccountName,noOfParallelJobs,image,osType,imagePublisher,imageOffer,imageSku,imageVersion,slaveLaunchMethod,initScript,adminUserName,adminPassword,virtualNetworkName,subnetName,retentionTimeInMin,jvmOptions" /> diff --git a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.properties b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.properties index 0b94df3..0e3f362 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.properties +++ b/src/main/resources/com/microsoftopentechnologies/azure/AzureSlaveTemplate/config.properties @@ -22,7 +22,10 @@ Image_Version=Image Version Launch_Method=Launch Method -Init_Script=Init Script +Initialization_Configuration=VM First Startup Configuration +Init_Script=Initialization Script +Execute_Init_Script_As_Root=Run Initialization Script As Root (Linux Only) +Do_Not_Use_Machine_If_Init_Fails=Don't Use VM If Initialization Script Fails (Linux Only) Username=Username Password=Password @@ -32,7 +35,7 @@ JVM_Options=JVM Options #CloudServiceName=Cloud Service Name RetentionTimeInMin=Retention Time (in minutes) -Template_Status=Slave Provisioning +Template_Is_Disabled=Disable template. Template_Status_Details=Provisioning Failure Reason Delete_Template=Delete Template Verify_Template=Verify Template diff --git a/src/main/resources/com/microsoftopentechnologies/azure/Messages.properties b/src/main/resources/com/microsoftopentechnologies/azure/Messages.properties index 97dd78d..69d5fa0 100644 --- a/src/main/resources/com/microsoftopentechnologies/azure/Messages.properties +++ b/src/main/resources/com/microsoftopentechnologies/azure/Messages.properties @@ -1,10 +1,12 @@ # Global configuration - validations Azure_Config_Success=Successfully verified Azure configuration. Azure_GC_InitScript_Warn_Msg=Ensure image is pre-configured with a Java runtime or provide a script to install Java in headless (silent) mode. \ - \nIf using JNLP, refer here for a sample script. + \nIf using JNLP, see README.md for a sample script. Azure_GC_LaunchMethod_Warn_Msg=Make sure the Azure slave can reach the master via the Jenkins URL. Refer to the help for details. Azure_GC_TemplateStatus_Warn_Msg=The template is marked as disabled. Check the template status details in the Advanced section. +Azure_GC_OS_Type_Unknown_Err=Unknown OS type. Should be Linux or Windows + Azure_GC_UserName_Err=Not a valid user name. The user name must contain between 3 and 15 characters: alphanumerics, the underscore or the hyphen. Azure_GC_Password_Err=Required: Not a valid password. Refer to the password rules in the help. Azure_GC_JVM_Option_Err=Error: Not a valid JVM Option. JVM options should start with a hyphen(-). e.g. -Xmx1500m @@ -15,7 +17,9 @@ Azure_GC_Template_Val_Profile_Err=Failed to validate the Azure profile. Verify t Azure_GC_Template_max_VM_Err=The current number of virtual machines in this Azure subscription is {0}, which is more than or equal to the default value {1} \ \n.Consider increasing Max Virtual Machines Limit value or delete existing virtual machines from your subscription. Azure_GC_Template_Null_Or_Empty=The template name is null or empty. -Azure_GC_Template_Name_NA=The cloud service name {0} is either not available or not valid. Use a different template name. +Azure_GC_Template_Name_Not_Valid=The template name is not valid. Must begin with a letter, and contain only letters, numbers, or dashes +Azure_GC_Template_Name_Shortened=The template name is valid, but VM names will be shortened to: {0} +Azure_GC_Template_LOC_Not_Found=The location is not valid Azure_GC_Template_Name_LOC_No_Match=The cloud service location and the location selected do not match. Use a different template or location. Azure_GC_Template_CS_NA=Cloud service name {0} is either not available or not valid. Use a different cloud service name. Azure_GC_Template_CS_LOC_No_Match=The cloud service location and the location selected do not match. Use a different cloud service or location. @@ -26,8 +30,9 @@ Azure_GC_Template_Executors_Not_Positive=The number of executors must be a posit Azure_GC_Template_RT_Null_Or_Empty=Missing retention time. Azure_GC_Template_RT_Not_Positive=The retention time must be a positive integer. Azure_GC_Template_ImageFamilyOrID_Null_Or_Empty=Missing image family or image ID. -Azure_GC_Template_ImageFamilyOrID_Not_Valid=Failed to validate the provided image family or image ID. Make sure to reference a image that is available. -Azure_GC_Template_ImageFamilyOrID_LOC_No_Match=The selected location is not among the locations where the image {0} is available. +Azure_GC_Template_ImageURI_Not_Valid=Failed to validate the provided image location. +Azure_GC_Template_ImageReference_Not_Valid=Failed to validate the provided image reference: {0} +Azure_GC_Template_ImageURI_Not_In_Same_Account=The image URI is not located in the same storage account as the target storage account for the VM Azure_GC_Template_JNLP_Not_Supported=The JNLP launch method is supported only for Windows. Azure_GC_Template_UN_Null_Or_Empty=Missing admin user name. Azure_GC_Template_PWD_Null_Or_Empty=Missing admin password. @@ -38,15 +43,18 @@ Azure_GC_Template_subnet_NotFound=The subnet {0} does not belong to the specifie Azure_Template_Config_Success=Verified the template configuration successfully. -# Used internally in code and may appear in slave configuration or template status details -Delete_Slave=Node is marked for deletion. -IDLE_TIMEOUT_SHUTDOWN="Node is stopped(Deallocated) by Jenkins after Idle timeout" +Failed_Initial_Shutdown_Or_Delete=Node failed initial shutdown/deletion. Marking as delete, will be cleaned up later. +Idle_Timeout_Shutdown=Node is being stopped(Deallocated) by Jenkins after idle timeout +Idle_Timeout_Delete=Node is being deleted by Jenkins after idle timeout +User_Delete=Node is being deleted by the user Slave_Failed_To_Connect=The slave failed to connect. The node has been marked for deletion. Make sure that the appropriate firewall exceptions have been configured \ for the slave to connect to the master. +Slave_Failed_Init_Script=The slave connected, but failed its initialization script. The node has been marked for deletion. +Shutdown_Slave_Failed_To_Revive=The previously shut down slave failed to start. # Post build action for deprovisioning -Azure_Slave_Post_Build_Action=Configure a post build action for the Azure slave. -Build_Action_Shutdown_Slave=Shutdown Azure slave. -Build_Action_Delete_Slave=Delete Azure slave after job execution. -Build_Action_Delete_Slave_If_Not_Success=Delete Azure slave if the build is not successful. +Azure_Slave_Post_Build_Action=Perform an action if the job was performed on an Azure Slave. +Build_Action_Shutdown_Slave=Shutdown Azure slave after build execution. +Build_Action_Delete_Slave=Delete slave after build execution (when idle). +Build_Action_Delete_Slave_If_Not_Success=Delete slave if the build was not successful (when idle). SA_Blank_Create_New=(Leave blank to create a new storage account) diff --git a/src/main/resources/customImageTemplate.json b/src/main/resources/customImageTemplate.json new file mode 100644 index 0000000..ad83e92 --- /dev/null +++ b/src/main/resources/customImageTemplate.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + }, + "variables": { + "virtualNetworkName": "jenkinsarm-vnet", + "subnetName": "jenkinsarm-snet", + "storageAccountName": "jenkinsarmst", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "addressPrefix": "10.0.0.0/16", + "subnetPrefix": "10.0.0.0/24", + "publicIPAddressType": "Dynamic", + "storageAccountContainerName": "vhds", + "storageAccountType": "Standard_LRS" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "properties": { + "accountType": "[variables('storageAccountType')]" + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [{ + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "properties": { + "publicIPAllocationMethod": "[variables('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" + } + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/networkInterfaces", + "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [{ + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + }] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[concat(variables('vmName'), copyIndex())]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computername": "[concat(variables('vmName'), copyIndex())]", + "adminUsername": "[variables('adminUsername')]", + "adminPassword": "[variables('adminPassword')]" + }, + "storageProfile": { + "osDisk": { + "name": "[concat(variables('vmName'), copyIndex())]", + "osType": "[variables('osType')]", + "caching": "ReadWrite", + "image": { + "uri": "[variables('image')]" + }, + "createOption": "FromImage", + "vhd": { + "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" + } + } + }, + "networkProfile": { + "networkInterfaces": [{ + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" + }] + } + } + }] +} \ No newline at end of file diff --git a/src/main/resources/customImageTemplateWithScript.json b/src/main/resources/customImageTemplateWithScript.json new file mode 100644 index 0000000..0797257 --- /dev/null +++ b/src/main/resources/customImageTemplateWithScript.json @@ -0,0 +1,170 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountKey" : { + "type" : "secureString" + } + }, + "variables": { + "virtualNetworkName": "jenkinsarm-vnet", + "subnetName": "jenkinsarm-snet", + "storageAccountName": "jenkinsarmst", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "addressPrefix": "10.0.0.0/16", + "subnetPrefix": "10.0.0.0/24", + "publicIPAddressType": "Dynamic", + "storageAccountContainerName": "vhds", + "storageAccountType": "Standard_LRS", + "startupScriptURI": "", + "startupScriptName": "", + "jenkinsServerURL": "", + "clientSecrets": [] + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "properties": { + "accountType": "[variables('storageAccountType')]" + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [{ + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "properties": { + "publicIPAllocationMethod": "[variables('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" + } + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/networkInterfaces", + "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [{ + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + }] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[concat(variables('vmName'), copyIndex())]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computername": "[concat(variables('vmName'), copyIndex())]", + "adminUsername": "[variables('adminUsername')]", + "adminPassword": "[variables('adminPassword')]" + }, + "storageProfile": { + "osDisk": { + "name": "[concat(variables('vmName'), copyIndex())]", + "osType": "[variables('osType')]", + "caching": "ReadWrite", + "image": { + "uri": "[variables('image')]" + }, + "createOption": "FromImage", + "vhd": { + "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" + } + } + }, + "networkProfile": { + "networkInterfaces": [{ + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" + }] + } + }, + "resources" : [ + { + "type": "extensions", + "name": "[concat('customScript', variables('vmName'), copyIndex())]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'), copyIndex())]" + ], + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.7", + "autoUpgradeMinorVersion": true, + "settings": { + "fileUris": [ + "[variables('startupScriptURI')]" + ], + "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File ', variables('startupScriptName'),' ', variables('jenkinsServerURL'),' ', variables('vmName'),copyIndex(),' ', variables('clientSecrets')[copyIndex()])]" + }, + "protectedSettings": { + "storageAccountName" : "[variables('storageAccountName')]", + "storageAccountKey" : "[parameters('storageAccountKey')]" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/referenceImageTemplate.json b/src/main/resources/referenceImageTemplate.json new file mode 100644 index 0000000..461024a --- /dev/null +++ b/src/main/resources/referenceImageTemplate.json @@ -0,0 +1,137 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + }, + "variables": { + "virtualNetworkName": "jenkinsarm-vnet", + "subnetName": "jenkinsarm-snet", + "storageAccountName": "jenkinsarmst", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "addressPrefix": "10.0.0.0/16", + "subnetPrefix": "10.0.0.0/24", + "publicIPAddressType": "Dynamic", + "storageAccountContainerName": "vhds", + "storageAccountType": "Standard_LRS" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "properties": { + "accountType": "[variables('storageAccountType')]" + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [{ + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "properties": { + "publicIPAllocationMethod": "[variables('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" + } + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/networkInterfaces", + "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [{ + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + }] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[concat(variables('vmName'), copyIndex())]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computername": "[concat(variables('vmName'), copyIndex())]", + "adminUsername": "[variables('adminUsername')]", + "adminPassword": "[variables('adminPassword')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "[variables('imagePublisher')]", + "offer": "[variables('imageOffer')]", + "sku": "[variables('imageSku')]", + "version": "latest" + }, + "osDisk": { + "name": "osdisk", + "vhd": { + "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [{ + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" + }] + } + } + }] +} \ No newline at end of file diff --git a/src/main/resources/referenceImageTemplateWithScript.json b/src/main/resources/referenceImageTemplateWithScript.json new file mode 100644 index 0000000..5693de3 --- /dev/null +++ b/src/main/resources/referenceImageTemplateWithScript.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountKey" : { + "type" : "secureString" + } + }, + "variables": { + "virtualNetworkName": "jenkinsarm-vnet", + "subnetName": "jenkinsarm-snet", + "storageAccountName": "jenkinsarmst", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "addressPrefix": "10.0.0.0/16", + "subnetPrefix": "10.0.0.0/24", + "publicIPAddressType": "Dynamic", + "storageAccountContainerName": "vhds", + "storageAccountType": "Standard_LRS", + "startupScriptURI": "", + "startupScriptName": "", + "jenkinsServerURL": "", + "clientSecrets": [] + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "properties": { + "accountType": "[variables('storageAccountType')]" + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [{ + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "properties": { + "publicIPAllocationMethod": "[variables('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" + } + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Network/networkInterfaces", + "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [{ + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + }] + } + }, + { + "apiVersion": "2015-05-01-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[concat(variables('vmName'), copyIndex())]", + "location": "[variables('location')]", + "copy": { + "name": "vmcopy", + "count": "[parameters('count')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computername": "[concat(variables('vmName'), copyIndex())]", + "adminUsername": "[variables('adminUsername')]", + "adminPassword": "[variables('adminPassword')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "[variables('imagePublisher')]", + "offer": "[variables('imageOffer')]", + "sku": "[variables('imageSku')]", + "version": "latest" + }, + "osDisk": { + "name": "osdisk", + "vhd": { + "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [{ + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" + }] + } + }, + "resources" : [ + { + "type": "extensions", + "name": "[concat('customScript', variables('vmName'), copyIndex())]", + "apiVersion": "2015-05-01-preview", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'), copyIndex())]" + ], + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.7", + "autoUpgradeMinorVersion": true, + "settings": { + "fileUris": [ + "[variables('startupScriptURI')]" + ], + "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File ', variables('startupScriptName'),' ', variables('jenkinsServerURL'),' ', variables('vmName'),copyIndex(),' ', variables('clientSecrets')[copyIndex()])]" + }, + "protectedSettings": { + "storageAccountName" : "[variables('storageAccountName')]", + "storageAccountKey" : "[parameters('storageAccountKey')]" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/scripts/azure.ps1 b/src/main/resources/scripts/azure.ps1 deleted file mode 100644 index f01d1d8..0000000 --- a/src/main/resources/scripts/azure.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -Set-ExecutionPolicy Unrestricted -$jenkinsServerUrl = $args[0] -$vmName = $args[1] -$secret = $args[2] - -$jenkinsSlaveJarUrl = $jenkinsServerUrl + "jnlpJars/slave.jar" -$jnlpUrl=$jenkinsServerUrl + 'computer/' + $vmName + '/slave-agent.jnlp' - -$baseDir = 'c:\azurecsdir' -$JDKUrl = 'http://azure.azulsystems.com/zulu/zulu1.7.0_51-7.3.0.4-win64.zip?jenkins' -$destinationJDKZipPath = $baseDir + '\zuluJDK.zip' -$destinationSlaveJarPath = $baseDir + '\slave.jar' -$javaExe = $baseDir + '\zulu1.7.0_51-7.3.0.4-win64\bin\java.exe' - -# Function to get path of script file -function Get-ScriptPath -{ - return $MyInvocation.ScriptName; -} - -# Checking if this is first time script is getting executed, if yes then downloading JDK -If(-not((Test-Path $destinationJDKZipPath))) -{ - md -Path $baseDir -Force - $wc = New-Object System.Net.WebClient - $wc.DownloadFile($JDKUrl, $destinationJDKZipPath) - - $shell_app = new-object -com shell.application - $zip_file = $shell_app.namespace($destinationJDKZipPath) - $javaInstallDir = $shell_app.namespace($baseDir) - $javaInstallDir.Copyhere($zip_file.items()) - - $wc = New-Object System.Net.WebClient - $wc.DownloadFile($jenkinsSlaveJarUrl, $destinationSlaveJarPath) - - $scriptPath = Get-ScriptPath - $content = 'powershell.exe -ExecutionPolicy Unrestricted -file' + ' '+ $scriptPath + ' '+ $jenkinsServerUrl + ' ' + $vmName + ' ' + $secret - $commandFile = $baseDir + '\slaveagenttask.cmd' - $content | Out-File $commandFile -Encoding ASCII -Append - schtasks /create /tn "Jenkins slave agent" /ru "SYSTEM" /sc onstart /rl HIGHEST /delay 0000:30 /tr $commandFile /f -} - -# Launching jenkins slave agent -$process = New-Object System.Diagnostics.Process; -$process.StartInfo.FileName = $javaExe; -If($secret) -{ - $process.StartInfo.Arguments = "-jar $destinationSlaveJarPath -secret $secret -jnlpUrl $jnlpUrl" -} -else -{ - $process.StartInfo.Arguments = "-jar $destinationSlaveJarPath -jnlpUrl $jnlpUrl" -} -$process.StartInfo.RedirectStandardError = $true; -$process.StartInfo.RedirectStandardOutput = $true; -$process.StartInfo.UseShellExecute = $false; -$process.StartInfo.CreateNoWindow = $true; - -$process.StartInfo; -$process.Start(); - -Write-Host 'Done Init Script.'; diff --git a/src/main/resources/scripts/init.ps1 b/src/main/resources/scripts/init.ps1 new file mode 100644 index 0000000..bcb4048 --- /dev/null +++ b/src/main/resources/scripts/init.ps1 @@ -0,0 +1,41 @@ +Set-ExecutionPolicy Unrestricted +$jenkinsServerUrl = $args[0] +$vmName = $args[1] +$secret = $args[2] + +$baseDir = 'C:\Jenkins' +mkdir $baseDir +# Download the JDK +$source = "http://download.oracle.com/otn-pub/java/jdk/7u79-b15/jdk-7u79-windows-x64.exe" +$destination = "$baseDir\jdk.exe" +$client = new-object System.Net.WebClient +$cookie = "oraclelicense=accept-securebackup-cookie" +$client.Headers.Add([System.Net.HttpRequestHeader]::Cookie, $cookie) +$client.downloadFile([string]$source, [string]$destination) + +# Execute the unattended install +$jdkInstallDir=$baseDir + '\jdk\' +$jreInstallDir=$baseDir + '\jre\' +C:\Jenkins\jdk.exe /s INSTALLDIR=$jdkInstallDir /INSTALLDIRPUBJRE=$jdkInstallDir + +$javaExe=$jdkInstallDir + '\bin\java.exe' +$jenkinsSlaveJarUrl = $jenkinsServerUrl + "jnlpJars/slave.jar" +$destinationSlaveJarPath = $baseDir + '\slave.jar' + +# Download the jar file +$client = new-object System.Net.WebClient +$client.DownloadFile($jenkinsSlaveJarUrl, $destinationSlaveJarPath) + +# Calculate the jnlpURL +$jnlpUrl = $jenkinsServerUrl + 'computer/' + $vmName + '/slave-agent.jnlp' + +while ($true) { + try { + # Launch + & $javaExe -jar $destinationSlaveJarPath -secret $secret -jnlpUrl $jnlpUrl -noReconnect + } + catch [System.Exception] { + Write-Output $_.Exception.ToString() + } + sleep 10 +} \ No newline at end of file diff --git a/src/main/resources/templateImageValue.json b/src/main/resources/templateImageValue.json deleted file mode 100644 index 94f2338..0000000 --- a/src/main/resources/templateImageValue.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", - "contentVersion": "1.0.0.0", - "parameters": { - }, - "variables": { - "virtualNetworkName": "jenkinsarm-vnet", - "subnetName": "jenkinsarm-snet", - "storageAccountName": "jenkinsarmst", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", - "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", - "addressPrefix": "10.0.0.0/16", - "subnetPrefix": "10.0.0.0/24", - "publicIPAddressType": "Dynamic", - "storageAccountContainerName": "vhds", - "storageAccountType": "Standard_LRS" - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2015-05-01-preview", - "location": "[variables('location')]", - "properties": { - "accountType": "[variables('storageAccountType')]" - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/virtualNetworks", - "name": "[variables('virtualNetworkName')]", - "location": "[variables('location')]", - "properties": { - "addressSpace": { - "addressPrefixes": [ - "[variables('addressPrefix')]" - ] - }, - "subnets": [{ - "name": "[variables('subnetName')]", - "properties": { - "addressPrefix": "[variables('subnetPrefix')]" - } - } - ] - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/publicIPAddresses", - "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "properties": { - "publicIPAllocationMethod": "[variables('publicIPAddressType')]", - "dnsSettings": { - "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" - } - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/networkInterfaces", - "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "dependsOn": [ - "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" - ], - "properties": { - "ipConfigurations": [{ - "name": "ipconfig1", - "properties": { - "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": { - "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" - }, - "subnet": { - "id": "[variables('subnetRef')]" - } - } - }] - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Compute/virtualMachines", - "name": "[concat(variables('vmName'), copyIndex())]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "dependsOn": [ - "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", - "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" - ], - "properties": { - "hardwareProfile": { - "vmSize": "[variables('vmSize')]" - }, - "osProfile": { - "computername": "[concat(variables('vmName'), copyIndex())]", - "adminUsername": "[variables('adminUsername')]", - "adminPassword": "[variables('adminPassword')]" - }, - "storageProfile": { - "osDisk": { - "name": "[concat(variables('vmName'), copyIndex())]", - "osType": "[variables('osType')]", - "caching": "ReadWrite", - "image": { - "uri": "[variables('image')]" - }, - "createOption": "FromImage", - "vhd": { - "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" - } - } - }, - "networkProfile": { - "networkInterfaces": [{ - "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" - }] - } - } - }] -} \ No newline at end of file diff --git a/src/main/resources/templateValue.json b/src/main/resources/templateValue.json deleted file mode 100644 index 3d660b9..0000000 --- a/src/main/resources/templateValue.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", - "contentVersion": "1.0.0.0", - "parameters": { - }, - "variables": { - "virtualNetworkName": "jenkinsarm-vnet", - "subnetName": "jenkinsarm-snet", - "storageAccountName": "jenkinsarmst", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", - "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", - "addressPrefix": "10.0.0.0/16", - "subnetPrefix": "10.0.0.0/24", - "publicIPAddressType": "Dynamic", - "storageAccountContainerName": "vhds", - "storageAccountType": "Standard_LRS" - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2015-05-01-preview", - "location": "[variables('location')]", - "properties": { - "accountType": "[variables('storageAccountType')]" - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/virtualNetworks", - "name": "[variables('virtualNetworkName')]", - "location": "[variables('location')]", - "properties": { - "addressSpace": { - "addressPrefixes": [ - "[variables('addressPrefix')]" - ] - }, - "subnets": [{ - "name": "[variables('subnetName')]", - "properties": { - "addressPrefix": "[variables('subnetPrefix')]" - } - } - ] - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/publicIPAddresses", - "name": "[concat(variables('vmName'), copyIndex(), 'IPName')]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "properties": { - "publicIPAllocationMethod": "[variables('publicIPAddressType')]", - "dnsSettings": { - "domainNameLabel": "[concat(variables('vmName'), copyIndex())]" - } - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Network/networkInterfaces", - "name": "[concat(variables('vmName'), copyIndex(), 'NIC')]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "dependsOn": [ - "[concat('Microsoft.Network/publicIPAddresses/', variables('vmName'), copyIndex(), 'IPName')]", - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" - ], - "properties": { - "ipConfigurations": [{ - "name": "ipconfig1", - "properties": { - "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": { - "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('vmName'), copyIndex(), 'IPName'))]" - }, - "subnet": { - "id": "[variables('subnetRef')]" - } - } - }] - } - }, - { - "apiVersion": "2015-05-01-preview", - "type": "Microsoft.Compute/virtualMachines", - "name": "[concat(variables('vmName'), copyIndex())]", - "location": "[variables('location')]", - "copy": { - "name": "vmcopy", - "count": "[parameters('count')]" - }, - "dependsOn": [ - "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", - "[concat('Microsoft.Network/networkInterfaces/', variables('vmName'), copyIndex(), 'NIC')]" - ], - "properties": { - "hardwareProfile": { - "vmSize": "[variables('vmSize')]" - }, - "osProfile": { - "computername": "[concat(variables('vmName'), copyIndex())]", - "adminUsername": "[variables('adminUsername')]", - "adminPassword": "[variables('adminPassword')]" - }, - "storageProfile": { - "imageReference": { - "publisher": "[variables('imagePublisher')]", - "offer": "[variables('imageOffer')]", - "sku": "[variables('imageSku')]", - "version": "latest" - }, - "osDisk": { - "name": "osdisk", - "vhd": { - "uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/',variables('storageAccountContainerName'),'/', variables('vmName'), copyIndex(), 'OSDisk.vhd')]" - }, - "caching": "ReadWrite", - "createOption": "FromImage" - } - }, - "networkProfile": { - "networkInterfaces": [{ - "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vmName'), copyIndex(), 'NIC'))]" - }] - } - } - }] -} \ No newline at end of file diff --git a/src/main/webapp/help-doNotUseMachineIfInitFails.html b/src/main/webapp/help-doNotUseMachineIfInitFails.html new file mode 100644 index 0000000..dcc77ee --- /dev/null +++ b/src/main/webapp/help-doNotUseMachineIfInitFails.html @@ -0,0 +1,4 @@ +
+ If checked, the Azure node will execute the startup script as root. + Currently applies to Linux nodes only. +
diff --git a/src/main/webapp/help-executeInitScriptAsRoot.html b/src/main/webapp/help-executeInitScriptAsRoot.html new file mode 100644 index 0000000..b67e8d8 --- /dev/null +++ b/src/main/webapp/help-executeInitScriptAsRoot.html @@ -0,0 +1,4 @@ +
+ If checked, the Azure node will be discarded if the initialization script returns a non-zero exit code. + Currently applies to Linux nodes only. +
diff --git a/src/main/webapp/help-initScript.html b/src/main/webapp/help-initScript.html index 6e31dc6..e672d25 100644 --- a/src/main/webapp/help-initScript.html +++ b/src/main/webapp/help-initScript.html @@ -1,19 +1,22 @@
-At a minimum, the init script needs to install a Java runtime.

+At a minimum, the init script needs to install a Java runtime.
Custom prepared images are recommended if the initialization script is taking more than 20 minutes to execute.

-Below are examples of initialization scripts:

+Below are examples of initialization scripts:

1) Ubuntu
    # Install Java
    sudo apt-get -y update
    sudo apt-get install -y openjdk-7-jdk
    sudo apt-get -y update --fix-missing
    sudo apt-get install -y openjdk-7-jdk

-2) For Windows slaves with JNLP launch, if no init script is provided then Jenkins will execute the default PowerShell script, - which will work only if anonymous access is allowed for the master machine.

- - If your master machine is configured with security options, refer to this script: - https://gist.github.com/snallami/5aa9ea2c57836a3b3635 - and edit it accordingly.
+2) Windows w/JNLP
+ For Windows slaves with JNLP launch, this script is a powershell script.
+ Automatically passed to this script is:
+ First argument - Jenkins server URL
+ Second argument - VMName
+ Third argument - JNLP secret, required if the server has security enabled.
+ You need to install Java, download the slave jar file from: '[server url]jnlpJars/slave.jar'.
+ The server url should already have a trailing slash. Then execute the following to connect:
+ java.exe -jar [slave jar location] [-secret [client secret if required]] [server url]computer/[vm name]/slave-agent.jnlp
diff --git a/src/main/webapp/help-maxVirtualMachinesLimit.html b/src/main/webapp/help-maxVirtualMachinesLimit.html index 4f6dd68..4551ea4 100644 --- a/src/main/webapp/help-maxVirtualMachinesLimit.html +++ b/src/main/webapp/help-maxVirtualMachinesLimit.html @@ -1,6 +1,6 @@
-Specify the maximum number of virtual machines that can be created in a subscription.
+Specify the maximum number of virtual machines that can be created.
-This includes the number of virtual machines that were created outside Jenkins. +This number only includes those machines that the current set of credentials has access to view and does NOT include classic VMs.
diff --git a/src/main/webapp/help-templateDisabled.html b/src/main/webapp/help-templateDisabled.html new file mode 100644 index 0000000..e44cf06 --- /dev/null +++ b/src/main/webapp/help-templateDisabled.html @@ -0,0 +1,3 @@ +
+If checked, no attempt will be made to allocate this template. +
diff --git a/src/main/webapp/help-templateStatus.html b/src/main/webapp/help-templateStatus.html deleted file mode 100644 index 9e6415d..0000000 --- a/src/main/webapp/help-templateStatus.html +++ /dev/null @@ -1,11 +0,0 @@ -
-If set to "Active until first failure", Jenkins will keep attempting to create slaves from the template during the job execution until the first unrecoverable error -is encountered, at which point this setting will be automatically changed to "Disabled". - -If set to "Disabled", the creation of slaves based on this template will be suspended. - -If set to "Active always", Jenkins will continue attempting to provision slaves based on this template regardless of any failures that may occur. Note that this setting may -result in an excessive allocation of resources for the malfunctioning slaves. Such a continuous slave provisioning process can be stopped manually by changing the slave -provisioning setting to "Disabled", or fixing the underlying issue causing the failures. - -