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

Commit

Permalink
Add extension for Windows patching at deployment (#3704)
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickLang authored and jackfrancis committed Aug 21, 2018
1 parent 997e9e3 commit ecc9b5b
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 0 deletions.
157 changes: 157 additions & 0 deletions extensions/windows-patches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Windows Patching Extension

This extension will install Windows Server patches, including prerelease hotfixes. It's useful for the following cases:

1. Microsoft support has provided a pre-release hotfix for your testing
2. Installing additional Windows update packages (MSU) that are not yet included in the default Windows Server with Containers VM on the Azure Marketplace.

## Configuration

|Name |Required| Acceptable Value |
|-------------------|--------|----------------------|
|name |yes | windows-patches |
|version |yes | v1 |
|rootURL |optional| `https://raw.githubusercontent.com/Azure/acs-engine/master/` or any repo with the same extensions/... directory structure |
|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'` |

## Example

```json
...
"agentPoolProfiles": [
{
"name": "windowspool1",
"extensions": [
{
"name": "windows-patches"
}
]
}
],
...
"extensionProfiles": [
{
"name": "windows-patches",
"version": "v1",
"rootURL": "https://raw.githubusercontent.com/Azure/acs-engine/master/",
"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'"
}
]
...
```

## Supported Orchestrators

This has been tested with Kubernetes clusters, and does not depend on any specific version.

## Selecting Patches

### Cumulative Updates

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.

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.
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.
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.
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.
5. This will pop up a new window with a hyperlink such as `windows10.0-kb4343909-x64_f931af6d56797388715fe3b0d97569af7aebdae6.msu`. Copy that link.
6. Include that link in the `extensionParameters` as shown in the [Example](#Example)

### Supplied by Microsoft support

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.

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
2. Copy the sample below for either bash (Linux, Mac, or WSL), or PowerShell (Windows), and modify the variables at the top.


#### Using az cli and bash to upload the patch

```bash
#!/bin/bash
export resource_group=patchgroup
export storage_location=westus2
export storage_account_name=privatepatches
export container_name=hotfix
export blob_name=Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe
export file_to_upload=Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe

echo "Creating the group..."
az group create --location $storage_location --resource-group $resource_group

echo "Creating the storage account..."
az storage account create --location $storage_location --name $storage_account_name --resource-group $resource_group --sku Standard_LRS

echo "Getting the connection string..."
export AZURE_STORAGE_CONNECTION_STRING="`az storage account show-connection-string --name $storage_account_name --resource-group $resource_group`"

echo "Creating the container..."
az storage container create --name $container_name

echo "Uploading the file..."
az storage blob upload --container-name $container_name --file $file_to_upload --name $blob_name

echo "Getting a read-only SAS token, good for 30 days..."
export EXPIRY=`date +"%Y-%m-%dT%H:%M:%SZ" -d '30 days'`
export TEMPORARY_SAS=`az storage blob generate-sas --container-name $container_name --name $blob_name --permissions r --expiry $EXPIRY`

echo "Getting a URL to the file..."
export ABSURL=`az storage blob url --container-name $container_name --name $blob_name --sas-token $TEMPORARY_SAS`

echo "Full URL including access token:"
echo "$ABSURL?$TEMPORARY_SAS" | sed "s/\"//g"
```

#### Using az cli and PowerShell to upload the patch

```powershell
$resource_group="patchgroup"
$storage_location="westus2"
$storage_account_name="privatepatches"
$container_name="hotfix"
$blob_name="Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe"
$file_to_upload="Windows10.0-KB999999-x64-InstallForTestingPurposesOnly.exe"
Write-Host "Creating the group..."
az group create --location $storage_location --resource-group $resource_group
Write-Host "Creating the storage account..."
az storage account create --location $storage_location --name $storage_account_name --resource-group $resource_group --sku Standard_LRS
Write-Host "Getting the connection string..."
$ENV:AZURE_STORAGE_CONNECTION_STRING = az storage account show-connection-string --name $storage_account_name --resource-group $resource_group
Write-Host "Creating the container..."
az storage container create --name $container_name
Write-Host "Uploading the file..."
az storage blob upload --container-name $container_name --file $file_to_upload --name $blob_name
Write-Host "Getting a read-only SAS token, good for 30 days..."
$EXPIRY = ([DateTime]::Now + [timespan]::FromDays(30)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$TEMPORARY_SAS = az storage blob generate-sas --container-name $container_name --name $blob_name --permissions r --expiry $EXPIRY
Write-Host "Getting a URL to the file..."
$ABSURL = az storage blob url --container-name $container_name --name $blob_name --sas-token $TEMPORARY_SAS
Write-Host "Full URL including access token:"
$full_url = "$($ABSURL)?$($TEMPORARY_SAS)".Replace("""","")
$full_url | Write-Host
$full_url | Set-Clipboard
```

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.

## Troubleshooting

Extension execution output is logged to files found under the following directory on the target virtual machine.

```powershell
C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension
```

The specified files are downloaded into the following directory on the target virtual machine.

```powershell
C:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.*\Downloads\<n>
```
85 changes: 85 additions & 0 deletions extensions/windows-patches/v1/installPatches.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

# Return codes:
# 0 - success
# 1 - install failure
# 2 - download failure
# 3 - unrecognized patch extension

param(
[string[]] $URIs
)

function DownloadFile([string] $URI, [string] $fullName)
{
try {
Write-Host "Downloading $URI"
Invoke-WebRequest -UseBasicParsing $URI -OutFile $fullName
} catch {
Write-Error $_
exit 2
}
}


$URIs | ForEach-Object {
Write-Host "Processing $_"
$uri = $_
$pathOnly = $uri
if ($pathOnly.Contains("?"))
{
$pathOnly = $pathOnly.Split("?")[0]
}
$fileName = Split-Path $pathOnly -Leaf
$ext = [io.path]::GetExtension($fileName)
$fullName = [io.path]::Combine($env:TEMP, $fileName)
switch ($ext) {
".exe" {
Start-Process -FilePath bcdedit.exe -ArgumentList "/set {current} testsigning on" -Wait
DownloadFile -URI $uri -fullName $fullName
Write-Host "Starting $fullName"
$proc = Start-Process -Passthru -FilePath "$fullName" -ArgumentList "/q /norestart"
Wait-Process -InputObject $proc
switch ($proc.ExitCode)
{
0 {
Write-Host "Finished running $fullName"
}
3010 {
Write-Host "Finished running $fullName. Reboot required to finish patching."
}
Default {
Write-Error "Error running $fullName, exitcode $($proc.ExitCode)"
exit 1
}
}
}
".msu" {
DownloadFile -URI $uri -fullName $fullName
Write-Host "Installing $localPath"
$proc = Start-Process -Passthru -FilePath wusa.exe -ArgumentList "$fullName /quiet /norestart"
Wait-Process -InputObject $proc
switch ($proc.ExitCode)
{
0 {
Write-Host "Finished running $fullName"
}
3010 {
Write-Host "Finished running $fullName. Reboot required to finish patching."
}
Default {
Write-Error "Error running $fullName, exitcode $($proc.ExitCode)"
exit 1
}
}
}
Default {
Write-Error "This script extension doesn't know how to install $ext files"
exit 3
}
}
}

# No failures, schedule reboot now

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
exit 0
1 change: 1 addition & 0 deletions extensions/windows-patches/v1/supported-orchestrators.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["Kubernetes"]
39 changes: 39 additions & 0 deletions extensions/windows-patches/v1/template-link.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "[concat(EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET), 'windows-patches')]",
"type": "Microsoft.Resources/deployments",
"apiVersion": "[variables('apiVersionLinkDefault')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET), '/extensions/cse', '-EXTENSION_TARGET_VM_TYPE-', copyIndex(EXTENSION_LOOP_OFFSET))]"
],
"copy": {
"count": "EXTENSION_LOOP_COUNT",
"name": "windows-patchesExtensionLoop"
},
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "EXTENSION_URL_REPLACEextensions/windows-patches/v1/template.json",
"contentVersion": "1.0.0.0"
},
"parameters": {
"artifactsLocation": {
"value": "EXTENSION_URL_REPLACE"
},
"apiVersionCompute": {
"value": "[variables('apiVersionDefault')]"
},
"targetVMName": {
"value": "[concat(EXTENSION_TARGET_VM_NAME_PREFIX, copyIndex(EXTENSION_LOOP_OFFSET))]"
},
"targetVMType": {
"value": "EXTENSION_TARGET_VM_TYPE"
},
"extensionParameters": {
"value": "EXTENSION_PARAMETERS_REPLACE"
},
"vmIndex":{
"value": "[copyIndex(EXTENSION_LOOP_OFFSET)]"
}
}
}
}
72 changes: 72 additions & 0 deletions extensions/windows-patches/v1/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"artifactsLocation": {
"type": "string",
"minLength": 1,
"metadata": {
"description": "Artifacts Location - URL"
}
},
"apiVersionCompute": {
"type": "string",
"minLength": 1,
"metadata": {
"description": "Compute API Version"
}
},
"targetVMName":{
"type": "string",
"minLength": 1,
"metadata": {
"description": "Name of the vm to run the extension on"
}
},
"targetVMType":{
"type": "string",
"minLength": 1,
"metadata": {
"description": "Type of the vm to run the extension: master or agent "
}
},
"extensionParameters": {
"type": "securestring",
"minLength": 0,
"metadata": {
"description": "Custom Parameter for Extension - comma delimited list of URIs"
}
},
"vmIndex": {
"type": "int",
"metadata": {
"description": "index in the pool of the current agent, used so that we can get the extension name right"
}
}
},
"variables": { },
"resources": [
{
"apiVersion": "[parameters('apiVersionCompute')]",
"dependsOn": [],
"location": "[resourceGroup().location]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(parameters('targetVMName'),'/cse', '-', parameters('targetVMType'), '-', parameters('vmIndex'))]",
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": "1.8",
"autoUpgradeMinorVersion": true,
"settings": {
"fileUris": [
"[concat(parameters('artifactsLocation'), 'extensions/windows-patches/v1/installPatches.ps1')]"
]
},
"protectedSettings": {
"commandToExecute": "[concat('powershell.exe -ExecutionPolicy bypass \"& ./installPatches.ps1 -URIs ', parameters('extensionParameters'), '\"')]"
}
}
}
],
"outputs": { }
}

0 comments on commit ecc9b5b

Please sign in to comment.