Skip to content

Commit 35ee7c5

Browse files
authored
Linux inbound agent support (#406)
1 parent 0e0f5f9 commit 35ee7c5

18 files changed

Lines changed: 244 additions & 76 deletions

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A Jenkins Plugin to create Jenkins agents in Azure Virtual Machines (via Azure A
77

88
Supported features:
99

10-
1. Windows Agents on Azure Cloud using SSH and JNLP
10+
1. Windows Agents on Azure Cloud using SSH and Inbound agent connection
1111
* For Windows images to launch via SSH, the image needs to be preconfigured with SSH.
1212
* For preparing custom windows image, refer to [Azure documentation](http://azure.microsoft.com/en-us/documentation/articles/virtual-machines-capture-image-windows-server/)
1313
2. Linux Agents on Azure Cloud using SSH
@@ -88,21 +88,20 @@ The built-in image only has a clean Windows or Ubuntu OS and some tools like Git
8888
* Use a custom user image (provide image URL and OS type - note, your custom image has to be available into the same storage account in which you are going to create agent nodes);
8989
* Using any marketplace image by specifying an image reference (provide image reference by publisher, offer, sku and version). You can get the publisher, offer and sku by looking at the ARM template of that image.
9090
3. For the launch method, select SSH or JNLP.
91-
* Linux agents can be launched only using SSH.
92-
* Windows agents can be launched using SSH or JNLP. For Windows agents, if the launch method is SSH then check Pre-Install SSH in Windows Agent or image needs to be custom-prepared with an SSH server pre-installed.
91+
* For Windows agents, if the launch method is SSH then check Pre-Install SSH in Windows Agent or image needs to be custom-prepared with an SSH server pre-installed.
9392

94-
We recommend to use SSH rather than JNLP, for you need less init codes and get much clearer logs.
95-
> When using the JNLP launch option, ensure the following:
93+
We recommend to use SSH rather than connecting it to the controller, it's simpler to setup and get much clearer logs.
94+
> When using the Inbound agent launch option, ensure the following:
9695
> * Jenkins URL (Manage Jenkins -> Configure System -> Jenkins Location)
9796
> * The URL needs to be reachable by the Azure agent, so make sure to configure any relevant firewall rules accordingly.
98-
> * TCP port for JNLP agent agents (Manage Jenkins -> Configure Global Security -> Enable security -> TCP port for JNLP agents).
99-
> * The TCP port needs to be reachable from the Azure agent launched using JNLP. It is recommended to use a fixed port so that any necessary firewall exceptions can be made.
97+
> * TCP port for Inbound agents (Manage Jenkins -> Configure Global Security -> Enable security -> TCP port for inbound agents).
98+
> * The TCP port needs to be reachable from the Azure agent launched with connecting to the controller. It is recommended to use a fixed port so that any necessary firewall exceptions can be made.
10099
>
101-
> If the Jenkins controller is running on Azure, then open an endpoint for "TCP port for JNLP agent agents" and,
100+
> If the Jenkins controller is running on Azure, then open an endpoint for "TCP port for Inbound agents" and,
102101
> in case of Windows, add the necessary firewall rules inside virtual machine (Run -> firewall.cpl).
103102
104-
4. For the Initialization Script, you can provide a script that will be executed after the VM is provisioned. This allows to install any app/tool you need on the agent. Please be noted you need to at least install JRE if the image does not have Java pre-installed.
105-
We prepared a sample script for Linux via SSH, Windows via SSH and Windows via JNLP. Please find details in help button.
103+
4. For the Initialization Script, you can provide a script that will be executed after the VM is provisioned. This allows to install any app/tool you need on the agent. Please be aware you need to at least install JRE if the image does not have Java pre-installed.
104+
We prepared a sample script for Linux via SSH, Windows via SSH and Windows via connecting to the controller. Please find details in help button.
106105

107106
If you hit the [storage scalability limits](https://docs.microsoft.com/en-us/azure/storage/storage-scalability-targets) for your custom images on the storage account where the VHD resides, you should consider using the agent's [temporary storage](https://blogs.msdn.microsoft.com/mast/2013/12/06/understanding-the-temporary-drive-on-windows-azure-virtual-machines) or copy your custom image in multiple storage accounts and use multiple VM templates with the same label within the same agent cloud.
108107

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
3+
JENKINS_URL=$1
4+
AGENT_NAME=$2
5+
SECRET=$3
6+
7+
mkdir /opt/jenkins
8+
9+
(
10+
cd /opt/jenkins
11+
apt-get update
12+
apt-get install -y default-jdk git
13+
14+
MAVEN_VERSION=3.9.1
15+
curl -O "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz"
16+
tar zxvf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt/
17+
ln -s /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/bin/mvn
18+
19+
mvn --version
20+
21+
curl -O "$JENKINS_URL/jnlpJars/agent.jar"
22+
23+
java -jar agent.jar -jnlpUrl "$JENKINS_URL/computer/$AGENT_NAME/jenkins-agent.jnlp" -secret "$SECRET" -workDir /opt/jenkins/workDir
24+
) |& tee /opt/jenkins/init-script.log

docs/init-scripts/windows-inbound-agent.ps1

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,28 @@ $jenkinsserverurl = $args[0]
3232
$vmname = $args[1]
3333
$secret = $args[2]
3434

35-
# Downloading jenkins agent jar
36-
Write-Output "Downloading jenkins agent jar "
37-
$slaveSource = $jenkinsserverurl + "jnlpJars/agent.jar"
38-
$destSource = "C:\agent.jar"
35+
# Download the service wrapper
36+
$wrapperExec = "c:\jenkins\jenkins-agent.exe"
37+
$configFile = "c:\jenkins\jenkins-agent.xml"
38+
$agentSource = $jenkinsserverurl + "jnlpJars/agent.jar"
39+
40+
mkdir C:\jenkins
3941
$wc = New-Object System.Net.WebClient
40-
$wc.DownloadFile($slaveSource, $destSource)
42+
$wc.DownloadFile("https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe", $wrapperExec)
43+
$wc.DownloadFile("https://raw.githubusercontent.com/Azure/jenkins/master/agents_scripts/jenkins-slave.exe.config", "c:\jenkins\jenkins-agent.exe.config")
44+
$wc.DownloadFile("https://raw.githubusercontent.com/Azure/jenkins/master/agents_scripts/jenkins-slave.xml", $configFile)
4145

42-
# execute agent
46+
# Prepare config
4347
Write-Output "Executing agent process "
44-
$java="java"
45-
$jar="-jar"
46-
$serverURL=$jenkinsserverurl + "computer/" + $vmname + '/jenkins-agent.jnlp'
47-
48-
# TODO look at porting the run as service part of the old script from https://raw.githubusercontent.com/Azure/jenkins/master/agents_scripts/Jenkins-Windows-Init-Script-Jnlp.ps1
49-
while ($true) {
50-
try {
51-
# Launch
52-
& $java -jar $destSource -secret $secret -jnlpUrl $serverURL
53-
}
54-
catch [System.Exception] {
55-
Write-Output $_.Exception.ToString()
56-
}
57-
Start-Sleep 10
48+
$configExec = "java"
49+
$configArgs = "-jnlpUrl `"${jenkinsserverurl}/computer/${vmname}/jenkins-agent.jnlp`" -workDir C:\jenkins\workDir"
50+
if ($secret) {
51+
$configArgs += " -secret `"$secret`""
5852
}
53+
(Get-Content $configFile).replace('@JAVA@', $configExec) | Set-Content $configFile
54+
(Get-Content $configFile).replace('@ARGS@', $configArgs) | Set-Content $configFile
55+
(Get-Content $configFile).replace('@SLAVE_JAR_URL', $agentSource) | Set-Content $configFile
56+
57+
# Install the service
58+
& $wrapperExec install
59+
& $wrapperExec start

src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,9 @@ public static String getBasicInitScript(AzureVMAgentTemplate template) {
735735
AzureVMManagementServiceDelegate.PRE_INSTALLED_TOOLS_SCRIPT
736736
.get(builtInImage).get(Constants.INSTALL_GIT));
737737
}
738-
if ((builtInImage.equals(Constants.UBUNTU_1604_LTS) || builtInImage.equals(Constants.UBUNTU_2004_LTS))
738+
if ((builtInImage.equals(Constants.UBUNTU_1604_LTS)
739+
|| builtInImage.equals(Constants.UBUNTU_2004_LTS)
740+
|| builtInImage.equals(Constants.UBUNTU_2204_LTS))
739741
&& template.isInstallDocker()) {
740742
stringBuilder.append(getSeparator(template.getOsType()));
741743
stringBuilder.append(
@@ -1545,8 +1547,8 @@ public ListBoxModel doFillUsageModeItems() throws IOException, ServletException
15451547

15461548
public ListBoxModel doFillAgentLaunchMethodItems() {
15471549
ListBoxModel model = new ListBoxModel();
1548-
model.add(Constants.LAUNCH_METHOD_SSH);
1549-
model.add(Constants.LAUNCH_METHOD_JNLP);
1550+
model.add(Messages.AzureVMAgentTemplate_SSH_Agent_Launch_Method(), Constants.LAUNCH_METHOD_SSH);
1551+
model.add(Messages.AzureVMAgentTemplate_Inbound_Agent_Launch_Method(), Constants.LAUNCH_METHOD_JNLP);
15501552

15511553
return model;
15521554
}
@@ -1584,6 +1586,7 @@ public ListBoxModel doFillExistingStorageAccountNameItems(
15841586

15851587
public ListBoxModel doFillBuiltInImageItems() {
15861588
ListBoxModel model = new ListBoxModel();
1589+
model.add(Constants.UBUNTU_2204_LTS);
15871590
model.add(Constants.UBUNTU_2004_LTS);
15881591
model.add(Constants.UBUNTU_1604_LTS);
15891592
model.add(Constants.WINDOWS_SERVER_2022);
@@ -1848,8 +1851,8 @@ public FormValidation doVerifyConfiguration(
18481851

18491852
// First validate the subscription info. If it is not correct,
18501853
// then we can't validate the
1851-
String result = AzureClientHolder.getDelegate(azureCredentialsId).verifyConfiguration(resourceGroupName,
1852-
maxVirtualMachinesLimit, deploymentTimeout);
1854+
String result = AzureClientHolder.getDelegate(azureCredentialsId)
1855+
.verifyConfiguration(resourceGroupName, deploymentTimeout);
18531856
if (!result.equals(Constants.OP_SUCCESS)) {
18541857
return FormValidation.error(result);
18551858
}

src/main/java/com/microsoft/azure/vmagent/AzureVMCloud.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,7 +1108,6 @@ public String getDefaultResourceGroupName() {
11081108
@RequirePOST
11091109
public FormValidation doVerifyConfiguration(
11101110
@QueryParameter String azureCredentialsId,
1111-
@QueryParameter String maxVirtualMachinesLimit,
11121111
@QueryParameter String deploymentTimeout,
11131112
@QueryParameter String resourceGroupReferenceType,
11141113
@QueryParameter String newResourceGroupName,
@@ -1123,7 +1122,7 @@ public FormValidation doVerifyConfiguration(
11231122
AzureResourceManager azureClient = AzureResourceManagerCache.get(azureCredentialsId);
11241123
final String validationResult = AzureVMManagementServiceDelegate
11251124
.getInstance(azureClient, azureCredentialsId)
1126-
.verifyConfiguration(resourceGroupName, maxVirtualMachinesLimit, deploymentTimeout);
1125+
.verifyConfiguration(resourceGroupName, deploymentTimeout);
11271126
if (!validationResult.equalsIgnoreCase(Constants.OP_SUCCESS)) {
11281127
return FormValidation.error(validationResult);
11291128
}

src/main/java/com/microsoft/azure/vmagent/AzureVMCloudVerificationTask.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ public static boolean verifyConfiguration(AzureVMCloud cloud) {
170170
// Check the sub and off we go
171171
String result = cloud.getServiceDelegate().verifyConfiguration(
172172
cloud.getResourceGroupName(),
173-
Integer.toString(cloud.getMaxVirtualMachinesLimit()),
174173
Integer.toString(cloud.getDeploymentTimeout()));
175174
if (!Constants.OP_SUCCESS.equals(result)) {
176175
LOGGER.log(getStaticNormalLoggingLevel(), "AzureVMCloudVerificationTask: verifyConfiguration: {0}", result);

src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,17 @@ public AzureVMDeploymentInfo createDeployment(
264264
&& (isBasic || referenceType == ImageReferenceType.REFERENCE
265265
|| template.isPreInstallSsh());
266266

267-
final boolean useCustomScriptExtension
267+
boolean windows = properties.get("osType").equals(Constants.OS_TYPE_WINDOWS);
268+
boolean inboundAgent = properties.get("agentLaunchMethod").equals(Constants.LAUNCH_METHOD_JNLP);
269+
boolean useCustomScriptExtension
268270
= preInstallSshInWindows
269-
|| properties.get("osType").equals(Constants.OS_TYPE_WINDOWS)
270-
&& !StringUtils.isBlank((String) properties.get("initScript"))
271-
&& properties.get("agentLaunchMethod").equals(Constants.LAUNCH_METHOD_JNLP);
271+
|| windows
272+
&& StringUtils.isNotBlank((String) properties.get("initScript"))
273+
&& inboundAgent;
274+
275+
if (!useCustomScriptExtension && !windows && inboundAgent) {
276+
useCustomScriptExtension = true;
277+
}
272278

273279
// check if a custom image id has been provided otherwise work with publisher and offer
274280
boolean useManagedDisk = diskType.equals(Constants.DISK_MANAGED);
@@ -316,6 +322,17 @@ public AzureVMDeploymentInfo createDeployment(
316322

317323
final JsonNode tmp = MAPPER.readTree(embeddedTemplate);
318324

325+
if (useCustomScriptExtension) {
326+
ArrayNode resources = (ArrayNode) tmp.get("resources");
327+
for (JsonNode resource : resources) {
328+
String type = resource.get("type").asText();
329+
if (type.contains("virtualMachine")) {
330+
ArrayNode vmResources = (ArrayNode) resource.get("resources");
331+
vmResources.remove(windows ? 1 : 0);
332+
}
333+
}
334+
}
335+
319336
// Add count variable for loop....
320337
final ObjectNode count = MAPPER.createObjectNode();
321338
count.put("type", "int");
@@ -1450,6 +1467,8 @@ private static Map<String, Map<String, String>> getDefaultImageProperties() {
14501467
imageProperties("Canonical", "UbuntuServer", "16.04-LTS", "16.04-LTS", Constants.OS_TYPE_LINUX));
14511468
imageProperties.put(Constants.UBUNTU_2004_LTS,
14521469
imageProperties("canonical", "0001-com-ubuntu-server-focal", "20_04-lts-gen2", "20_04-lts-gen2", Constants.OS_TYPE_LINUX));
1470+
imageProperties.put(Constants.UBUNTU_2204_LTS,
1471+
imageProperties("canonical", "0001-com-ubuntu-server-jammy", "22_04-lts-gen2", "22_04-lts-gen2", Constants.OS_TYPE_LINUX));
14531472

14541473
return imageProperties;
14551474
}
@@ -1479,12 +1498,14 @@ private static Map<String, Map<String, String>> getPreInstalledToolsScript() {
14791498
tools.put(Constants.WINDOWS_SERVER_2022, new HashMap<>());
14801499
tools.put(Constants.UBUNTU_1604_LTS, new HashMap<>());
14811500
tools.put(Constants.UBUNTU_2004_LTS, new HashMap<>());
1501+
tools.put(Constants.UBUNTU_2204_LTS, new HashMap<>());
14821502
try {
14831503
windows(Constants.WINDOWS_SERVER_2016, tools);
14841504
windows(Constants.WINDOWS_SERVER_2019, tools);
14851505
windows(Constants.WINDOWS_SERVER_2022, tools);
14861506
ubuntu(Constants.UBUNTU_1604_LTS, tools);
14871507
ubuntu(Constants.UBUNTU_2004_LTS, tools);
1508+
ubuntu(Constants.UBUNTU_2204_LTS, tools);
14881509

14891510
return tools;
14901511
} catch (IOException e) {
@@ -1575,13 +1596,11 @@ public Set<String> getVMSizes(String location) {
15751596
* Validates certificate configuration.
15761597
*
15771598
* @param resourceGroupName Resource group name.
1578-
* @param maxVMLimit Max limit of virtual machines.
15791599
* @param timeOut Timeout of the verification.
15801600
* @return Verification result.
15811601
*/
15821602
public String verifyConfiguration(
15831603
String resourceGroupName,
1584-
String maxVMLimit,
15851604
String timeOut) {
15861605
try {
15871606
if (!AzureUtil.isValidTimeOut(timeOut)) {
@@ -1593,11 +1612,7 @@ public String verifyConfiguration(
15931612
return "Error: " + Messages.Azure_GC_Template_ResourceGroupName_Err();
15941613
}
15951614

1596-
if (!AzureUtil.isValidMAxVMLimit(maxVMLimit)) {
1597-
return "Invalid Limit, Should be a positive number, e.g. " + Constants.DEFAULT_MAX_VM_LIMIT;
1598-
}
1599-
1600-
if (!(AzureUtil.isValidTimeOut(timeOut) && AzureUtil.isValidMAxVMLimit(maxVMLimit)
1615+
if (!(AzureUtil.isValidTimeOut(timeOut)
16011616
&& AzureUtil.isValidResourceGroupName(resourceGroupName))) {
16021617
return Messages.Azure_GC_Template_Val_Profile_Err();
16031618
}

src/main/java/com/microsoft/azure/vmagent/util/AzureUtil.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -426,19 +426,6 @@ public static boolean isValidResourceGroupName(String resourceGroupName) {
426426
return false;
427427
}
428428

429-
/**
430-
* Checks if the maximum virtual machines limit is valid.
431-
*
432-
* @param maxVMLimit Maximum Virtual Limit
433-
* @return true if it is valid else return false
434-
*/
435-
public static boolean isValidMAxVMLimit(String maxVMLimit) {
436-
if (StringUtils.isBlank(maxVMLimit) || !maxVMLimit.matches(Constants.REG_EX_DIGIT)) {
437-
return false;
438-
}
439-
return true;
440-
}
441-
442429
/**
443430
* Checks if the deployment Timeout is valid.
444431
*

src/main/java/com/microsoft/azure/vmagent/util/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public final class Constants {
121121
public static final String WINDOWS_SERVER_2016 = "Windows Server 2016";
122122
public static final String UBUNTU_1604_LTS = "Ubuntu 16.04 LTS";
123123
public static final String UBUNTU_2004_LTS = "Ubuntu 20.04 LTS";
124+
public static final String UBUNTU_2204_LTS = "Ubuntu 22.04 LTS";
124125

125126
/**
126127
* ResourceGroup reference type.

src/main/resources/com/microsoft/azure/vmagent/Messages.properties

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Global configuration - validations
22
Azure_Config_Success=Successfully verified Azure configuration.
33
Azure_GC_InitScript_Warn_Msg=Ensure image is pre-configured with a Java runtime or provide a script \
4-
to install Java in headless (silent) mode. \
5-
\nIf using JNLP, see README.md for a sample script.
4+
to install Java in headless (silent) mode.
65
Azure_GC_LaunchMethod_Warn_Msg=Make sure the Azure agent can reach the controller via the Jenkins URL. \
76
Refer to the help for details.
87
Azure_GC_TemplateStatus_Warn_Msg=The template is marked as disabled. Check the template status details in \
@@ -65,7 +64,6 @@ Azure_GC_Template_ImageID_Not_Valid=The provided Image ID does not exist
6564
Azure_GC_Template_Gallery_Image_Not_Found=The target gallery image does not exist
6665
Azure_GC_Template_ImageURI_Not_In_Same_Account=The image URI is not located in the same storage account as the target \
6766
storage account for the VM
68-
Azure_GC_Template_JNLP_Not_Supported=The JNLP launch method is supported only for Windows.
6967
Azure_GC_Template_UN_Null_Or_Empty=Missing admin user name.
7068
Azure_GC_Template_PWD_Null_Or_Empty=Missing admin password.
7169
Azure_GC_Template_PWD_Not_Valid=Required: Not a valid password. The password length must be between 12 and 123 \
@@ -103,3 +101,7 @@ SA_Blank_Create_New=(Leave blank to create a new storage account)
103101
Azure_VM_Agent_Attach_Public_IP_Success=Successfully attached a public IP
104102
Azure_VM_Agent_Attach_Public_IP_Failure=Failed to attach a public IP
105103
Azure_GC_Template_NSG_NotFound=The Network Security Group {0} does not exist in the Resource Group.
104+
105+
AzureVMAgentTemplate.Inbound_Agent_Launch_Method=Launch agent by connecting it to the controller
106+
AzureVMAgentTemplate.SSH_Agent_Launch_Method=Launch agents via SSH
107+

0 commit comments

Comments
 (0)