Skip to content

Commit

Permalink
Add VMSS support (#587)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Nov 24, 2024
1 parent 7c08d4b commit b34ad99
Show file tree
Hide file tree
Showing 26 changed files with 287 additions and 75 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,7 @@ The plugin creates a Jenkins logger called `Azure VM Agent (Auto)`, update that
The ARM template which is used to deploy resources will now show up.

More information on Jenkins logs can be found in the Jenkins documentation for [Viewing logs](https://www.jenkins.io/doc/book/system-administration/viewing-logs/).

### Jenkins times out connecting to the agent when using a Public IP address

Ensure you've configured an NSG on the subnet that Jenkins is using or selected one in the advanced configuration of the VM template.
54 changes: 44 additions & 10 deletions src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.azure.core.http.rest.PagedIterable;
import com.azure.resourcemanager.AzureResourceManager;
import com.azure.resourcemanager.compute.models.DiskSkuTypes;
import com.azure.resourcemanager.network.models.NetworkSecurityGroup;
import com.azure.resourcemanager.storage.models.SkuName;
import com.azure.resourcemanager.storage.models.StorageAccount;
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
Expand Down Expand Up @@ -674,16 +675,11 @@ public static Map<String, Object> getTemplateProperties(AzureVMAgentTemplate tem
isBasic ? getBasicInitScript(template) : template.getInitScript());
templateProperties.put("terminateScript",
isBasic ? "" : template.getTerminateScript());
templateProperties.put("virtualNetworkName",
isBasic ? "" : template.getVirtualNetworkName());
templateProperties.put("virtualNetworkResourceGroupName",
isBasic ? "" : template.getVirtualNetworkResourceGroupName());
templateProperties.put("subnetName",
isBasic ? "" : template.getSubnetName());
templateProperties.put("usePrivateIP",
isBasic ? false : template.getUsePrivateIP());
templateProperties.put("nsgName",
isBasic ? "" : template.getNsgName());
templateProperties.put("virtualNetworkName", template.getVirtualNetworkName());
templateProperties.put("virtualNetworkResourceGroupName", template.getVirtualNetworkResourceGroupName());
templateProperties.put("subnetName", template.getSubnetName());
templateProperties.put("usePrivateIP", template.getUsePrivateIP());
templateProperties.put("nsgName", template.getNsgName());
templateProperties.put("jvmOptions",
isBasic ? "" : template.getJvmOptions());
templateProperties.put("noOfParallelJobs",
Expand Down Expand Up @@ -1430,6 +1426,44 @@ public List<Descriptor<RetentionStrategy<?>>> getAzureVMRetentionStrategy() {
return list;
}

@POST
public ListBoxModel doFillNsgNameItems(
@QueryParameter("cloudName") String cloudName) {
Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);

ListBoxModel model = new ListBoxModel();
model.add("--- Select Network Security Group in current resource group ---", "");

AzureVMCloud cloud = getAzureCloud(cloudName);
if (cloud == null) {
return model;
}

String azureCredentialsId = cloud.getAzureCredentialsId();
if (StringUtils.isBlank(azureCredentialsId)) {
return model;
}

String resourceGroupReferenceType = cloud.getResourceGroupReferenceType();
String newResourceGroupName = cloud.getNewResourceGroupName();
String existingResourceGroupName = cloud.getExistingResourceGroupName();

try {
AzureResourceManager azureClient = AzureResourceManagerCache.get(azureCredentialsId);
String resourceGroupName = AzureVMCloud.getResourceGroupName(
resourceGroupReferenceType, newResourceGroupName, existingResourceGroupName);
PagedIterable<NetworkSecurityGroup> nsgs =
azureClient.networkSecurityGroups()
.listByResourceGroup(resourceGroupName);
for (NetworkSecurityGroup nsg : nsgs) {
model.add(nsg.name());
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Cannot list availability set: ", e);
}
return model;

Check warning on line 1464 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 678-1464 are not covered by tests
}

@POST
public ListBoxModel doFillVirtualMachineSizeItems(
@QueryParameter("cloudName") String cloudName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ public AzureVMAgent createProvisionedAgent(
LOGGER.log(Level.INFO, "Waiting for deployment {0} with VM {1} to be completed",
new Object[]{deploymentName, vmName});

final int sleepTimeInSeconds = 30;
final int sleepTimeInSeconds = 5;

Check warning on line 538 in src/main/java/com/microsoft/azure/vmagent/AzureVMCloud.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 538 is not covered by tests
final int timeoutInSeconds = getDeploymentTimeout();
final int maxTries = timeoutInSeconds / sleepTimeInSeconds;
int triesLeft = maxTries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import com.jcraft.jsch.OpenSSHConfig;
import com.microsoft.azure.vmagent.availability.AvailabilitySet;
import com.microsoft.azure.vmagent.availability.AzureAvailabilityType;
import com.microsoft.azure.vmagent.availability.VirtualMachineScaleSet;
import com.microsoft.azure.vmagent.exceptions.AzureCloudException;
import com.microsoft.azure.vmagent.launcher.AzureComputerLauncher;
import com.microsoft.azure.vmagent.launcher.AzureSSHLauncher;
Expand Down Expand Up @@ -240,9 +241,6 @@ public AzureVMDeploymentInfo createDeployment(
final boolean ephemeralOSDisk = template.isEphemeralOSDisk();
final boolean encryptionAtHost = template.isEncryptionAtHost();
final int osDiskSize = template.getOsDiskSize();
final AzureAvailabilityType availabilityType = template.getAvailabilityType();
final String availabilitySet = availabilityType instanceof AvailabilitySet ?
((AvailabilitySet) availabilityType).getName() : null;

if (!template.getResourceGroupName().matches(Constants.DEFAULT_RESOURCE_GROUP_PATTERN)) {
LOGGER.log(Level.SEVERE, "ResourceGroup Name {0} is invalid. It should be 1-64 alphanumeric characters",
Expand Down Expand Up @@ -437,12 +435,21 @@ public AzureVMDeploymentInfo createDeployment(
boolean uamiEnabled = template.isEnableUAMI();

boolean osDiskSizeChanged = osDiskSize > 0;

final AzureAvailabilityType availabilityType = template.getAvailabilityType();
final String availabilitySet = availabilityType instanceof AvailabilitySet ?
((AvailabilitySet) availabilityType).getName() : null;

final String vmssName = availabilityType instanceof VirtualMachineScaleSet ?
((VirtualMachineScaleSet) availabilityType).getName() : null;

boolean availabilitySetEnabled = availabilitySet != null;
boolean vmssEnabled = vmssName != null;
boolean isSpecializedImage = false;
if (template.getImageReference() != null) {
isSpecializedImage = template.getImageReference().getGalleryImageSpecialized();
}
if (msiEnabled || uamiEnabled || osDiskSizeChanged || availabilitySetEnabled || isSpecializedImage) {
if (msiEnabled || uamiEnabled || osDiskSizeChanged || availabilitySetEnabled || isSpecializedImage || vmssEnabled) {
ArrayNode resources = (ArrayNode) tmp.get("resources");
for (JsonNode resource : resources) {
String type = resource.get("type").asText();
Expand Down Expand Up @@ -486,6 +493,14 @@ public AzureVMDeploymentInfo createDeployment(
((ObjectNode) propertiesNode).replace("availabilitySet",
availabilitySetNode);
}
if (vmssEnabled) {
ObjectNode vmssNode = MAPPER.createObjectNode();
vmssNode.put("id", String.format(
"[resourceId('Microsoft.Compute/virtualMachineScaleSets', '%s')]", vmssName));
JsonNode propertiesNode = resource.get("properties");
((ObjectNode) propertiesNode).replace("virtualMachineScaleSet",
vmssNode);
}
if (isSpecializedImage) {
// For specialized image remove the osProfile from the properties of the VirtualMachine resource
JsonNode propertiesNode = resource.get("properties");
Expand Down Expand Up @@ -643,7 +658,19 @@ public AzureVMDeploymentInfo createDeployment(
}

if (!(Boolean) properties.get("usePrivateIP")) {
addPublicIPResourceNode(tmp, tags);
List<String> availabilityZones = new ArrayList<>();
if (availabilityType instanceof VirtualMachineScaleSet) {
var name = ((VirtualMachineScaleSet) availabilityType).getName();

availabilityZones = azureClient.virtualMachineScaleSets()
.getByResourceGroup(resourceGroupName, name)
.availabilityZones()
.stream()
.map(ExpandableStringEnum::toString)
.collect(Collectors.toList());
}

addPublicIPResourceNode(tmp, tags, availabilityZones);
}

if (template.isAcceleratedNetworking()) {
Expand Down Expand Up @@ -888,13 +915,22 @@ private static void copyVariableIfNotBlank(JsonNode template, Map<String, Object

private void addPublicIPResourceNode(
JsonNode template,
List<AzureTagPair> tags) throws IOException {
List<AzureTagPair> tags,
List<String> availabilityZones) throws IOException {

final String ipName = "variables('vmName'), copyIndex(), 'IPName'";
try (InputStream fragmentStream =
AzureVMManagementServiceDelegate.class.getResourceAsStream(PUBLIC_IP_FRAGMENT_FILENAME)) {

final JsonNode publicIPFragment = MAPPER.readTree(fragmentStream);
final ObjectNode publicIPFragment = (ObjectNode) MAPPER.readTree(fragmentStream);
if (!availabilityZones.isEmpty()) {
ArrayNode zones = MAPPER.createArrayNode();
for (String zone : availabilityZones) {
zones.add(zone);
}
publicIPFragment.set("zones", zones);

Check warning on line 931 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 439-931 are not covered by tests
}

injectCustomTag(publicIPFragment, tags);
// Add the virtual network fragment
((ArrayNode) template.get("resources")).add(publicIPFragment);
Expand Down Expand Up @@ -935,6 +971,7 @@ private void addPublicIPResourceNode(
+ "))]");
ObjectNode ipAddressPropertiesNode = MAPPER.createObjectNode();
ipAddressPropertiesNode.put("deleteOption", "Delete");

publicIPIdNode.set("properties", ipAddressPropertiesNode);
propertiesNode.set("publicIPAddress", publicIPIdNode);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public String getDisplayName() {
}

@POST
public ListBoxModel doFillAvailabilitySetItems(
public ListBoxModel doFillNameItems(
@RelativePath("..") @QueryParameter("cloudName") String cloudName,
@RelativePath("..") @QueryParameter String location) {
Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.microsoft.azure.vmagent.availability;

import com.azure.core.http.rest.PagedIterable;
import com.azure.resourcemanager.AzureResourceManager;
import com.azure.resourcemanager.compute.models.OrchestrationMode;
import com.microsoft.azure.vmagent.AzureVMCloud;
import com.microsoft.azure.vmagent.Messages;
import com.microsoft.jenkins.credentials.AzureResourceManagerCache;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.RelativePath;
import hudson.model.Descriptor;
import hudson.slaves.Cloud;
import hudson.util.ListBoxModel;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;

public class VirtualMachineScaleSet extends AzureAvailabilityType {

private static final Logger LOGGER = Logger.getLogger(VirtualMachineScaleSet.class.getName());

private final String name;

@DataBoundConstructor
public VirtualMachineScaleSet(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
VirtualMachineScaleSet that = (VirtualMachineScaleSet) o;
return Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hashCode(name);

Check warning on line 53 in src/main/java/com/microsoft/azure/vmagent/availability/VirtualMachineScaleSet.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 26-53 are not covered by tests
}

@Extension
public static final class DescriptorImpl extends Descriptor<AzureAvailabilityType> {
@NonNull
@Override
public String getDisplayName() {
return Messages.scaleSet();
}

@POST
public ListBoxModel doFillNameItems(
@RelativePath("..") @QueryParameter("cloudName") String cloudName,
@RelativePath("..") @QueryParameter String location) {
Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);

ListBoxModel model = new ListBoxModel();
model.add("--- Select Scale Set in current resource group and location ---", "");

AzureVMCloud cloud = getAzureCloud(cloudName);
if (cloud == null) {
return model;
}

String azureCredentialsId = cloud.getAzureCredentialsId();
if (StringUtils.isBlank(azureCredentialsId)) {
return model;
}

String resourceGroupReferenceType = cloud.getResourceGroupReferenceType();
String newResourceGroupName = cloud.getNewResourceGroupName();
String existingResourceGroupName = cloud.getExistingResourceGroupName();

try {
AzureResourceManager azureClient = AzureResourceManagerCache.get(azureCredentialsId);
String resourceGroupName = AzureVMCloud.getResourceGroupName(
resourceGroupReferenceType, newResourceGroupName, existingResourceGroupName);
PagedIterable<com.azure.resourcemanager.compute.models.VirtualMachineScaleSet> scaleSets =
azureClient.virtualMachineScaleSets()
.listByResourceGroup(resourceGroupName);
for (com.azure.resourcemanager.compute.models.VirtualMachineScaleSet set : scaleSets) {
String region = set.region().label();
if (region.equals(location) && set.orchestrationMode() == OrchestrationMode.FLEXIBLE) {
model.add(set.name());
}
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Cannot list availability set: ", e);
}
return model;
}

private AzureVMCloud getAzureCloud(String cloudName) {
Cloud cloud = Jenkins.get().getCloud(cloudName);

if (cloud instanceof AzureVMCloud) {
return (AzureVMCloud) cloud;
}

return null;

Check warning on line 113 in src/main/java/com/microsoft/azure/vmagent/availability/VirtualMachineScaleSet.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 68-113 are not covered by tests
}

}
}
Loading

0 comments on commit b34ad99

Please sign in to comment.