Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Commit ecc9b5b

Browse files
PatrickLangjackfrancis
authored andcommitted
Add extension for Windows patching at deployment (#3704)
1 parent 997e9e3 commit ecc9b5b

File tree

5 files changed

+354
-0
lines changed

5 files changed

+354
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Windows Patching Extension
2+
3+
This extension will install Windows Server patches, including prerelease hotfixes. It's useful for the following cases:
4+
5+
1. Microsoft support has provided a pre-release hotfix for your testing
6+
2. Installing additional Windows update packages (MSU) that are not yet included in the default Windows Server with Containers VM on the Azure Marketplace.
7+
8+
## Configuration
9+
10+
|Name |Required| Acceptable Value |
11+
|-------------------|--------|----------------------|
12+
|name |yes | windows-patches |
13+
|version |yes | v1 |
14+
|rootURL |optional| `https://raw.githubusercontent.com/Azure/acs-engine/master/` or any repo with the same extensions/... directory structure |
15+
|extensionParameters|yes | comma-delimited list of URIs enclosed with ' such as `'https://privateupdates.domain.ext/Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe', 'https://privateupdates.domain.ext/Windows10.0-KB123456-x64-InstallForTestingPurposesOnly.exe'` |
16+
17+
## Example
18+
19+
```json
20+
...
21+
"agentPoolProfiles": [
22+
{
23+
"name": "windowspool1",
24+
"extensions": [
25+
{
26+
"name": "windows-patches"
27+
}
28+
]
29+
}
30+
],
31+
...
32+
"extensionProfiles": [
33+
{
34+
"name": "windows-patches",
35+
"version": "v1",
36+
"rootURL": "https://raw.githubusercontent.com/Azure/acs-engine/master/",
37+
"extensionParameters": "'https://mypatches.blob.core.windows.net/hotfix3692/Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe?sp=r&st=2018-08-17T00:25:01Z&se=2018-09-17T08:25:01Z&spr=https&sv=2017-11-09&sig=0000000000%3D&sr=b', 'http://download.windowsupdate.com/c/msdownload/update/software/secu/2018/08/windows10.0-kb4343909-x64_f931af6d56797388715fe3b0d97569af7aebdae6.msu'"
38+
}
39+
]
40+
...
41+
```
42+
43+
## Supported Orchestrators
44+
45+
This has been tested with Kubernetes clusters, and does not depend on any specific version.
46+
47+
## Selecting Patches
48+
49+
### Cumulative Updates
50+
51+
If you would like to include a cumulative update as part of your deployment that isn't in the Windows Server with Containers image, then follow these steps.
52+
53+
1. Browse to [Windows 10 Update History](https://support.microsoft.com/en-us/help/4099479), and follow the link to the right version (1709 or 1803) in the left. This page should be titled "Windows 10 and Windows Server update history" because the links also lead you to Windows Server updates.
54+
2. Next, look for the latest patch in the lower left such as ["August 14, 2018—KB4343909 (OS Build 17134.228)"](https://support.microsoft.com/en-us/help/4343909), and click that link.
55+
3. Scroll down to "How to get this update", and click on the ["Microsoft Update Catalog"](http://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343909) link.
56+
4. Find the row for `(year)-(month) Cumulative Update for Windows Server 2016 ((1709 or 1803)) for x64-based Systems (KB######)`, and click the "Download" button.
57+
5. This will pop up a new window with a hyperlink such as `windows10.0-kb4343909-x64_f931af6d56797388715fe3b0d97569af7aebdae6.msu`. Copy that link.
58+
6. Include that link in the `extensionParameters` as shown in the [Example](#Example)
59+
60+
### Supplied by Microsoft support
61+
62+
Once you have downloaded a private hotfix from Microsoft support, it needs to be put in an Azure-accessible location. The easiest way to do this is to create an [Azure Blob Storage](https://docs.microsoft.com/en-us/azure/storage/common/storage-create-storage-account#blob-storage-accounts) account. Once uploaded, you can create a private link with a key to access it that will work with this extension.
63+
64+
1. If you haven't already, install the [Azure CLI](https://docs.microsoft.com/cli/azure/get-started-with-az-cli2), and run `az login` to log in to Azure
65+
2. Copy the sample below for either bash (Linux, Mac, or WSL), or PowerShell (Windows), and modify the variables at the top.
66+
67+
68+
#### Using az cli and bash to upload the patch
69+
70+
```bash
71+
#!/bin/bash
72+
export resource_group=patchgroup
73+
export storage_location=westus2
74+
export storage_account_name=privatepatches
75+
export container_name=hotfix
76+
export blob_name=Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe
77+
export file_to_upload=Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe
78+
79+
echo "Creating the group..."
80+
az group create --location $storage_location --resource-group $resource_group
81+
82+
echo "Creating the storage account..."
83+
az storage account create --location $storage_location --name $storage_account_name --resource-group $resource_group --sku Standard_LRS
84+
85+
echo "Getting the connection string..."
86+
export AZURE_STORAGE_CONNECTION_STRING="`az storage account show-connection-string --name $storage_account_name --resource-group $resource_group`"
87+
88+
echo "Creating the container..."
89+
az storage container create --name $container_name
90+
91+
echo "Uploading the file..."
92+
az storage blob upload --container-name $container_name --file $file_to_upload --name $blob_name
93+
94+
echo "Getting a read-only SAS token, good for 30 days..."
95+
export EXPIRY=`date +"%Y-%m-%dT%H:%M:%SZ" -d '30 days'`
96+
export TEMPORARY_SAS=`az storage blob generate-sas --container-name $container_name --name $blob_name --permissions r --expiry $EXPIRY`
97+
98+
echo "Getting a URL to the file..."
99+
export ABSURL=`az storage blob url --container-name $container_name --name $blob_name --sas-token $TEMPORARY_SAS`
100+
101+
echo "Full URL including access token:"
102+
echo "$ABSURL?$TEMPORARY_SAS" | sed "s/\"//g"
103+
```
104+
105+
#### Using az cli and PowerShell to upload the patch
106+
107+
```powershell
108+
$resource_group="patchgroup"
109+
$storage_location="westus2"
110+
$storage_account_name="privatepatches"
111+
$container_name="hotfix"
112+
$blob_name="Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe"
113+
$file_to_upload="Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe"
114+
115+
Write-Host "Creating the group..."
116+
az group create --location $storage_location --resource-group $resource_group
117+
118+
Write-Host "Creating the storage account..."
119+
az storage account create --location $storage_location --name $storage_account_name --resource-group $resource_group --sku Standard_LRS
120+
121+
Write-Host "Getting the connection string..."
122+
$ENV:AZURE_STORAGE_CONNECTION_STRING = az storage account show-connection-string --name $storage_account_name --resource-group $resource_group
123+
124+
Write-Host "Creating the container..."
125+
az storage container create --name $container_name
126+
127+
Write-Host "Uploading the file..."
128+
az storage blob upload --container-name $container_name --file $file_to_upload --name $blob_name
129+
130+
Write-Host "Getting a read-only SAS token, good for 30 days..."
131+
$EXPIRY = ([DateTime]::Now + [timespan]::FromDays(30)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
132+
$TEMPORARY_SAS = az storage blob generate-sas --container-name $container_name --name $blob_name --permissions r --expiry $EXPIRY
133+
134+
Write-Host "Getting a URL to the file..."
135+
$ABSURL = az storage blob url --container-name $container_name --name $blob_name --sas-token $TEMPORARY_SAS
136+
137+
Write-Host "Full URL including access token:"
138+
$full_url = "$($ABSURL)?$($TEMPORARY_SAS)".Replace("""","")
139+
$full_url | Write-Host
140+
$full_url | Set-Clipboard
141+
```
142+
143+
The last line of the script will output a URL, and also put it on the Windows clipboard. Copy it into `extensionParameters` as shown in the sample above. Do not share this URL, keep it private.
144+
145+
## Troubleshooting
146+
147+
Extension execution output is logged to files found under the following directory on the target virtual machine.
148+
149+
```powershell
150+
C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension
151+
```
152+
153+
The specified files are downloaded into the following directory on the target virtual machine.
154+
155+
```powershell
156+
C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.*\Downloads\<n>
157+
```
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
# Return codes:
3+
# 0 - success
4+
# 1 - install failure
5+
# 2 - download failure
6+
# 3 - unrecognized patch extension
7+
8+
param(
9+
[string[]] $URIs
10+
)
11+
12+
function DownloadFile([string] $URI, [string] $fullName)
13+
{
14+
try {
15+
Write-Host "Downloading $URI"
16+
Invoke-WebRequest -UseBasicParsing $URI -OutFile $fullName
17+
} catch {
18+
Write-Error $_
19+
exit 2
20+
}
21+
}
22+
23+
24+
$URIs | ForEach-Object {
25+
Write-Host "Processing $_"
26+
$uri = $_
27+
$pathOnly = $uri
28+
if ($pathOnly.Contains("?"))
29+
{
30+
$pathOnly = $pathOnly.Split("?")[0]
31+
}
32+
$fileName = Split-Path $pathOnly -Leaf
33+
$ext = [io.path]::GetExtension($fileName)
34+
$fullName = [io.path]::Combine($env:TEMP, $fileName)
35+
switch ($ext) {
36+
".exe" {
37+
Start-Process -FilePath bcdedit.exe -ArgumentList "/set {current} testsigning on" -Wait
38+
DownloadFile -URI $uri -fullName $fullName
39+
Write-Host "Starting $fullName"
40+
$proc = Start-Process -Passthru -FilePath "$fullName" -ArgumentList "/q /norestart"
41+
Wait-Process -InputObject $proc
42+
switch ($proc.ExitCode)
43+
{
44+
0 {
45+
Write-Host "Finished running $fullName"
46+
}
47+
3010 {
48+
Write-Host "Finished running $fullName. Reboot required to finish patching."
49+
}
50+
Default {
51+
Write-Error "Error running $fullName, exitcode $($proc.ExitCode)"
52+
exit 1
53+
}
54+
}
55+
}
56+
".msu" {
57+
DownloadFile -URI $uri -fullName $fullName
58+
Write-Host "Installing $localPath"
59+
$proc = Start-Process -Passthru -FilePath wusa.exe -ArgumentList "$fullName /quiet /norestart"
60+
Wait-Process -InputObject $proc
61+
switch ($proc.ExitCode)
62+
{
63+
0 {
64+
Write-Host "Finished running $fullName"
65+
}
66+
3010 {
67+
Write-Host "Finished running $fullName. Reboot required to finish patching."
68+
}
69+
Default {
70+
Write-Error "Error running $fullName, exitcode $($proc.ExitCode)"
71+
exit 1
72+
}
73+
}
74+
}
75+
Default {
76+
Write-Error "This script extension doesn't know how to install $ext files"
77+
exit 3
78+
}
79+
}
80+
}
81+
82+
# No failures, schedule reboot now
83+
84+
schtasks /create /TN RebootAfterPatch /RU SYSTEM /TR "shutdown.exe /r /t 0 /d 2:17" /SC ONCE /ST $(([System.DateTime]::Now + [timespan]::FromMinutes(5)).ToString("HH:mm")) /V1 /Z
85+
exit 0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["Kubernetes"]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "[concat(EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET), 'windows-patches')]",
3+
"type": "Microsoft.Resources/deployments",
4+
"apiVersion": "[variables('apiVersionLinkDefault')]",
5+
"dependsOn": [
6+
"[concat('Microsoft.Compute/virtualMachines/', EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET), '/extensions/cse', '-EXTENSION_TARGET_VM_TYPE-', copyIndex(EXTENSION_LOOP_OFFSET))]"
7+
],
8+
"copy": {
9+
"count": "EXTENSION_LOOP_COUNT",
10+
"name": "windows-patchesExtensionLoop"
11+
},
12+
"properties": {
13+
"mode": "Incremental",
14+
"templateLink": {
15+
"uri": "EXTENSION_URL_REPLACEextensions/windows-patches/v1/template.json",
16+
"contentVersion": "1.0.0.0"
17+
},
18+
"parameters": {
19+
"artifactsLocation": {
20+
"value": "EXTENSION_URL_REPLACE"
21+
},
22+
"apiVersionCompute": {
23+
"value": "[variables('apiVersionDefault')]"
24+
},
25+
"targetVMName": {
26+
"value": "[concat(EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET))]"
27+
},
28+
"targetVMType": {
29+
"value": "EXTENSION_TARGET_VM_TYPE"
30+
},
31+
"extensionParameters": {
32+
"value": "EXTENSION_PARAMETERS_REPLACE"
33+
},
34+
"vmIndex":{
35+
"value": "[copyIndex(EXTENSION_LOOP_OFFSET)]"
36+
}
37+
}
38+
}
39+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
3+
"contentVersion": "1.0.0.0",
4+
"parameters": {
5+
"artifactsLocation": {
6+
"type": "string",
7+
"minLength": 1,
8+
"metadata": {
9+
"description": "Artifacts Location - URL"
10+
}
11+
},
12+
"apiVersionCompute": {
13+
"type": "string",
14+
"minLength": 1,
15+
"metadata": {
16+
"description": "Compute API Version"
17+
}
18+
},
19+
"targetVMName":{
20+
"type": "string",
21+
"minLength": 1,
22+
"metadata": {
23+
"description": "Name of the vm to run the extension on"
24+
}
25+
},
26+
"targetVMType":{
27+
"type": "string",
28+
"minLength": 1,
29+
"metadata": {
30+
"description": "Type of the vm to run the extension: master or agent "
31+
}
32+
},
33+
"extensionParameters": {
34+
"type": "securestring",
35+
"minLength": 0,
36+
"metadata": {
37+
"description": "Custom Parameter for Extension - comma delimited list of URIs"
38+
}
39+
},
40+
"vmIndex": {
41+
"type": "int",
42+
"metadata": {
43+
"description": "index in the pool of the current agent, used so that we can get the extension name right"
44+
}
45+
}
46+
},
47+
"variables": { },
48+
"resources": [
49+
{
50+
"apiVersion": "[parameters('apiVersionCompute')]",
51+
"dependsOn": [],
52+
"location": "[resourceGroup().location]",
53+
"type": "Microsoft.Compute/virtualMachines/extensions",
54+
"name": "[concat(parameters('targetVMName'),'/cse', '-', parameters('targetVMType'), '-', parameters('vmIndex'))]",
55+
"properties": {
56+
"publisher": "Microsoft.Compute",
57+
"type": "CustomScriptExtension",
58+
"typeHandlerVersion": "1.8",
59+
"autoUpgradeMinorVersion": true,
60+
"settings": {
61+
"fileUris": [
62+
"[concat(parameters('artifactsLocation'), 'extensions/windows-patches/v1/installPatches.ps1')]"
63+
]
64+
},
65+
"protectedSettings": {
66+
"commandToExecute": "[concat('powershell.exe -ExecutionPolicy bypass \"& ./installPatches.ps1 -URIs ', parameters('extensionParameters'), '\"')]"
67+
}
68+
}
69+
}
70+
],
71+
"outputs": { }
72+
}

0 commit comments

Comments
 (0)