initial commit

main
tseed 2022-10-26 19:05:05 +01:00
commit a79cac8cd8
93 changed files with 40162 additions and 0 deletions

214
ARM_templates/README.md Executable file
View File

@ -0,0 +1,214 @@
## Azure Information
#### tenant
67bda7ee-fd80-41ef-ac91-358418290a1e # nottingham
#### subscriptions
GBUoN-uks-Dev 8a6722e9-035b-4b46-9408-ff040ff063e2 # nottingham dev
Production 1a3c8479-0046-4a6f-a2ad-397cb9a6f931 # limited access to use / list vnet+subnet and obtain ip (prod network)
Research Managed 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c # nottingham research (prod)
#### research (prod) resource groups
rg-svc-rem-we-spp-1 # where the azure instances, vnic and resources reside
rg-vn-rem-we-1 # where the vnet+subnet reside for the vnic
#### dev resource group
UI-SPP-DEV-001 # where the azure instances, vnic and resources reside
## login / get bearer token to the tenant (dev)
When provisioning resources in the GBUoN-uks-Dev subscription ensure your account has been delegated the contributor role to resource group UI-SPP-DEV-001.
This method of authorization will give a URL that accepts the displayed device code then ask to authenticate (in this case with your nottingham.ac.uk account)
az login --tenant 67bda7ee-fd80-41ef-ac91-358418290a1e --use-device-code
SPN id: f45a0e1e-3f7f-44e8-971d-a56b563ef589
SPN secret: I5aAwcc9bIE]]kmhPIQpp5JCK1BiTg?]
az login --service-principal --username f45a0e1e-3f7f-44e8-971d-a56b563ef589 --password "I5aAwcc9bIE]]kmhPIQpp5JCK1BiTg?]" --tenant 67bda7ee-fd80-41ef-ac91-358418290a1e
## login / get bearer token using service principal credentials for the tenant (prod)
SPN id: ccb8dfce-f33d-4b09-9ab3-5acc1c43f368
SPN secret: 622zlS45N.6_q.f6hy~5zgL.F53M6we7Qg
az login --service-principal --username ccb8dfce-f33d-4b09-9ab3-5acc1c43f368 --password "622zlS45N.6_q.f6hy~5zgL.F53M6we7Qg" --tenant 67bda7ee-fd80-41ef-ac91-358418290a1e
## switch default subscription
az account set --subscription 8a6722e9-035b-4b46-9408-ff040ff063e2 # nottingham dev
az account set --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c # nottingham research (prod)
## list available locations for subscription
Prod will use default location of "westeurope", ARM templates expect the display name "West Europe".
Dev will use default location of "uksouth", ARM templates expect the display name "UK South".
az account list-locations
## find resources in subscriptions
az group list # whichever subscription is set
az group list --subscription 8a6722e9-035b-4b46-9408-ff040ff063e2 # nottingham dev
az group list --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c # nottingham research (prod)
## find vnet in resource group
Prod vnet vn-rem-we-1 in resource group rg-vn-rem-we-1, this is used for ip address on vnics, no public ip's maybe allocated.
Research (prod) rg-svc-rem-we-spp-1 should not contain any vnet or accociated subnet, if one is created public ip's can be allocated but ensure an nsg is not shared for hosts with nics in different resource group vnets.
az network vnet list | jq . # we see vnet in vn-rem-we-1
# we see subnets rg-vn-rem-we-1 | sn-vn-rem-we-1-frontend-1 | AzureBastionSubnet | sn-vn-rem-we-1-midtier-1 | vn-man-we-1, SPN may only use sn-vn-rem-we-1-midtier-1
az network vnet list --resource-group rg-svc-rem-we-spp-1
## Create Azure storage blob to host installer scripts for prod 1.1 ARM templates
This is not required for the dev / prod 1.0 ARM templates, this change was to facilitate the carbon black install in the latest prod 1.1 ARM templates for only Windows Server and Ubuntu Server.
#### Create storage account
```
az storage account create --name extensionartefact --resource-group rg-svc-rem-we-spp-1 --location "West Europe" --sku Standard_ZRS --https-only true --min-tls-version TLS1_2 --publish-internet-endpoints false --publish-microsoft-endpoints true --routing-choice MicrosoftRouting --encryption-services blob --tags 'CFManaged=false'
```
#### Create blob container specifying auth mode
```
az storage container create --resource-group rg-svc-rem-we-spp-1 --public-access off --account-name extensionartefact --name extensionartefact --auth-mode key
az storage container list --account-name extensionartefact
```
#### Upload contents of the extensionartefact directory in this repo to the container
Retrieve a storage account key: `az storage account keys list --resource-group rg-svc-rem-we-spp-1 --account-name extensionartefact`
```
az storage blob upload-batch --destination extensionartefact --source /\<local-path-to-artefacts\>/extensionartefacts --account-name extensionartefact --account-key "\<place-key-here\>"
```
#### Setup managed identity
```
az identity create --name extensionartefact --resource-group rg-svc-rem-we-spp-1 --location "West Europe"
az identity list
az identity show --resource-group rg-svc-rem-we-spp-1 --name extensionartefact
```
#### Assign managed identity to container or storage account
https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-cli
This managed identity will be used with the ARM templates to download the custom script extension script and also download the carbon black package from the container during script runtime.
* Retrieve the clientId of the managed identity for the assignee parameter: `az identity show --resource-group rg-svc-rem-we-spp-1 --name extensionartefact`
* Check name of desired role definition, only "Storage Blob Data Reader" should be required in this scenario: `az role definition list`
* Retrieve the id of the managed identity for the scope paramter: `az storage account list`
```
az role assignment create --assignee "87360148-9f10-45a2-a5ce-32d7c1134bd8" --role "Storage Blob Data Reader" --scope "/subscriptions/5d0ffc51-cdb8-4de5-a76c-fca19f5b300c/resourceGroups/rg-svc-rem-we-spp-1/providers/Microsoft.Storage/storageAccounts/extensionartefact"
```
The SPN will have insufficient privilidges to assign the rights, use your UoN account to obtain a bearer token to authenticate to complete the command.
## Deploy ARM templates from command line
Ensure you are logged into the correct tenant and using the desired subscription.
To test prod subscription functionality run the templates with the following minimal parameters;
az deployment group create --resource-group rg-svc-rem-we-spp-1 --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c --parameters "{\"adminUsername\": {\"value\": \"uonadmin\"}, \"adminPassword\": {\"value\": \"Z3qiaFJcbH2EC5DletL9\"},\"location\": {\"value\": \"West Europe\"}, \"networkResourceGroup\": {\"value\": \"rg-vn-rem-we-1\"}, \"networkSecurityGroupName\": {\"value\": \"CFLinux\"}, \"virtualNetworkName\": {\"value\": \"vn-rem-we-1\"}, \"subnetName\": {\"value\": \"sn-vn-rem-we-1-midtier-1\"}}" --template-file Azure_RHEL_instance.json
To test prod subscription functionality run the templates with all parameters to replicate CloudForms invocation;
az deployment group create --resource-group rg-svc-rem-we-spp-1 --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c --parameters "{\"adminUsername\": {\"value\": \"uonadmin\"}, \"adminPassword\": {\"value\": \"Z3qiaFJcbH2EC5DletL9\"},\"location\": {\"value\": \"West Europe\"}, \"email\": {\"value\": \"ucats@exmail.nottingham.ac.uk\"}, \"networkResourceGroup\": {\"value\": \"rg-vn-rem-we-1\"}, \"networkSecurityGroupName\": {\"value\": \"CFLinux\"}, \"virtualNetworkName\": {\"value\": \"vn-rem-we-1\"}, \"subnetName\": {\"value\": \"sn-vn-rem-we-1-midtier-1\"},\"imageUrn\": {\"value\": \"RedHat:RHEL:7.8:latest\"},\"projectCode\": {\"value\": \"UoN1234\"}, \"toggleShutdownSchedule\": {\"value\": \"t\"}, \"vmSize\": {\"value\": \"Standard_B4s\"}, \"location\": {\"value\": \"West Europe\"}, \"dataDiskType\": {\"value\": \"StandardSSD_LRS\"}, \"dataDiskSizeGB\": {\"value\": \"512\"}}" --template-file Azure_RHEL_instance.json
az deployment group create --resource-group rg-svc-rem-we-spp-1 --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c --parameters "{\"adminUsername\": {\"value\": \"uonadmin\"}, \"adminPassword\": {\"value\": \"Z3qiaFJcbH2EC5DletL9\"},\"location\": {\"value\": \"West Europe\"}, \"email\": {\"value\": \"ucats@exmail.nottingham.ac.uk\"}, \"networkResourceGroup\": {\"value\": \"rg-vn-rem-we-1\"}, \"networkSecurityGroupName\": {\"value\": \"CFLinux\"}, \"virtualNetworkName\": {\"value\": \"vn-rem-we-1\"}, \"subnetName\": {\"value\": \"sn-vn-rem-we-1-midtier-1\"},\"imageUrn\": {\"value\": \"Canonical:UbuntuServer:18.04-LTS:latest\"},\"projectCode\": {\"value\": \"UoN1234\"}, \"toggleShutdownSchedule\": {\"value\": \"t\"}, \"vmSize\": {\"value\": \"Standard_D8s_v3\"}, \"location\": {\"value\": \"West Europe\"}, \"dataDiskType\": {\"value\": \"StandardSSD_LRS\"}, \"dataDiskSizeGB\": {\"value\": \"512\"}}" --template-file Azure_UbuntuServer_instance.json
az deployment group create --resource-group rg-svc-rem-we-spp-1 --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c --parameters "{\"adminUsername\": {\"value\": \"uonadmin\"}, \"adminPassword\": {\"value\": \"Z3qiaFJcbH2EC5DletL9\"},\"location\": {\"value\": \"West Europe\"}, \"email\": {\"value\": \"ucats@exmail.nottingham.ac.uk\"}, \"networkResourceGroup\": {\"value\": \"rg-vn-rem-we-1\"}, \"networkSecurityGroupName\": {\"value\": \"CFWindows\"}, \"virtualNetworkName\": {\"value\": \"vn-rem-we-1\"}, \"subnetName\": {\"value\": \"sn-vn-rem-we-1-midtier-1\"},\"imageUrn\": {\"value\": \"MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest\"},\"projectCode\": {\"value\": \"UoN1234\"}, \"toggleShutdownSchedule\": {\"value\": \"t\"}, \"vmSize\": {\"value\": \"Standard_NC12\"}, \"location\": {\"value\": \"West Europe\"}, \"dataDiskType\": {\"value\": \"StandardSSD_LRS\"}, \"dataDiskSizeGB\": {\"value\": \"512\"}}" --template-file Azure_WindowsServer_instance.json
To test prod subscription functionality of a security group template to replicate CloudForms invocation;
az deployment group create --resource-group rg-svc-rem-we-spp-1 --subscription 5d0ffc51-cdb8-4de5-a76c-fca19f5b300c --parameters "{\"location\": {\"value\": \"West Europe\"}, \"networkSecurityGroupName\": {\"value\": \"rg-vn-rem-we-1\"}}" --template-file CFLinux_Azure_network_security_group.json
To test dev subscription functionality run the templates with all parameters to replicate CloudForms invocation;
az deployment group create --resource-group UI-SPP-DEV-001 --subscription 8a6722e9-035b-4b46-9408-ff040ff063e2 --parameters "{\"location\": {\"value\": \"UK South\"}, \"virtualNetworkName\": {\"value\": \"UI-SPP-DEV-001-vnet\"}, \"subnetName\": {\"value\": \"default\"}, \"networkSecurityGroupName\": {\"value\": \"CFLinux\"}, \"adminUsername\": {\"value\": \"uonadmin\"}, \"adminPassword\": {\"value\": \"Z3qiaFJcbH2EC5DletL9\"}, \"email\": {\"value\": \"ucats@exmail.nottingham.ac.uk\"}, \"projectCode\": {\"value\": \"1234\"}, \"toggleShutdownSchedule\": {\"value\": \"f\"}}" --template-file Azure_UbuntuServer_instance_noscript_pubip.json
#### parameter behaviour
If networkResourceGroup is ommited the template will assume the vnet+subnet reside within the same resource group as the instance.
If email is ommited the windows ARM template will not make group membership changes or initialize the data disk, the linux ARM template will populate the sssd acl and sudoers entries with the user 'unused'.
If projectCode is ommited the Cost Code tag will display 'not classified'.
If toggleShutdownSchedule is ommited it assumes default value of false 'f' and no shutdown policy will be applied.
Other parameters have default values that maybe overriden to suit the desired instance spec, however the custom script extensions are likely to fail as they are very specific to the UoN research managed environment.
## Template Information
#### Prod 1.0 Templates
Azure_RHEL_instance.json
Azure_UbuntuServer_instance.json
Azure_WindowsServer_instance.json
CFLinux_Azure_network_security_group.json linux network security group, allows ingress SSH tcp 22
CFWindows_Azure_network_security_group.json linux network security group, allows ingress RDP tcp 3389
Azure_UbuntuServer_customscript.sh Ubuntu prep disk, join domain, install MATE desktop, shutdown timer, nvidia drivers extension
Azure_RHEL_customscript.sh Redhat prep disk, join domain, install MATE desktop, shutdown timer, nvidia drivers extension
#### Prod 1.1 Templates
Azure_UbuntuServer_instance.json updated ARM template to use custom script extension using blob hosted setup script Azure_UbuntuServer_customscript.sh
Azure_WindowsServer_instance.json updated ARM template to use custom sctipt extension using blob hosted setup script Azure_WindowsServer_customscript.ps1
extensionartefacts/ content of the blob container, setup scripts and UoN private packages such as Carbon Black and license key
#### Dev Templates
\*The dev rev1/rev2 or prod network security group templates are suitable for use in the UI-SPP-DEV-001 resource group\*
\*The rev2 templates are equivalent to the prod templates wihtout the additional disk or shutdown timer extension they are suitable for the rg-svc-rem-we-spp-1 resource group\*
\*The rev3 Azure_UbuntuServer_instance_noscript_pubip.json template is a backported prod template for use on the CloudForms dev appliance using the UI-SPP-DEV-001 resource group, boot scripts disabled due to not domain connectivity this is intended as a starting point for future tempate changes that need to be tested in dev first\*
rev1/\*templates\* templates for instance and network security group in the UI-SPP-DEV-001 resource group
rev2/\*templates\* templates for instance and network security group in the rg-svc-rem-we-spp-1 resource group
rev3/\*templates\* derived from prod with the addition of a public ip but scripts disabled, in the UI-SPP-DEV-001 resource group
#### json vs yaml
The templates are written in yaml and converted to json with yarn. Conversion operates both ways, it is helpful to take example json arm templates and convert to yaml.
Building templates in json is tedious and error prone, if a yarn sucessfully converts a template to json but does not run on the cli try an online json lint website to find syntax errors. If the template fails on the cli with no useful information run the template in Azure custom deployment for more debug, https://portal.azure.com/#create/Microsoft.Template.
usage;
https://github.com/Azure/azure-quickstart-templates Yaml allows comments and is much easier to read. https://github.com/TeamYARM/YARM-CLI
./Yarm.ConsoleApp.exe -i CFInstance_win.yaml
CFInstance_win.yaml => CFInstance_win.json
#### Prod 1.0 customscript extensions
These scripts prepare the data disk, join the domain and install MATE desktop / browsers for Linux or install browsers and change key settings for Windows, they are specific to the UoN research managed environment.
For the Prod 1.0 templates these scripts were base64 encoded and included inline within the respective templates.
Azure_RHEL_customscript.sh
Azure_UbuntuServer_customscript.sh
#### Prod 1.1 customscript extensions
These scripts prepare the data disk, join the domain, install the carbon black service and install MATE desktop / browsers for Linux or install browsers and change key settings for Windows, they are specific to the UoN research managed environment.
The templates no longer use inline scripts or commands, instead electing to use an Azure managed identity to pull the installer scripts and carbon black packages for an Azure storage blob, this was necessary as the carbon black package is not publicly avaialable and the license key file was required to be kept private.
extensionartefacts/Azure_UbuntuServer_customscript.sh
extensionartefacts/Azure_WindowsServer_customscript.ps1
#### create customscript extension script property
This is only used with the Prod 1.0 Linux ARM templates, the script is base64 encoded and seeded into the template.
https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-linux
cat rhel_customscript_extension.sh | gzip -9 | base64 -w 0

View File

@ -0,0 +1,201 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "RedHat:RHEL:7.3:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "User name for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B1ls",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "UI-SPP-DEV-001-vnet",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "default",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "UK South",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
}
},
"variables": {
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"publicIPAddressName": "[concat(variables('resourcePrefix'), '-pip')]",
"publicIPAddressType": "Dynamic",
"publicIpAddressSku": "Basic",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
"subnetRef": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/publicIPAddresses",
"name": "[variables('publicIPAddressName')]",
"location": "[parameters('location')]",
"properties": {
"publicIPAddressVersion": "IPv4",
"publicIPAllocationMethod": "[variables('publicIPAddressType')]",
"idleTimeoutInMinutes": 4
},
"sku": {
"name": "[variables('publicIpAddressSku')]"
}
},
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true",
"Owner": "ucats",
"OwnerMail": "ucats@nottingham.ac.uk",
"CFRequestID": "1234",
"CFServiceID": "1234",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
}
]
}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
}
}
}

View File

@ -0,0 +1,147 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
# cloudform issue converting json to yaml with string field containing integer (sku field), pass urn as string and split in template to workaround
imageUrn:
type: string
defaultValue: "RedHat:RHEL:7.3:latest"
metadata:
description: "az vm image list --output table / az vm image list -p RedHat --all --output table"
adminUsername:
type: string
metadata:
description: User name for the Virtual Machine.
adminPassword:
type: securestring
metadata:
description: Password for the Virtual Machine.
vmSize:
type: string
defaultValue: Standard_B1ls
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
virtualNetworkName:
type: string
defaultValue: UI-SPP-DEV-001-vnet
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: default
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: UK South
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: "[utcNow()]"
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
variables:
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
publicIPAddressName: "[concat(variables('resourcePrefix'), '-pip')]"
publicIPAddressType: Dynamic
publicIpAddressSku: Basic
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/publicIPAddresses
name: "[variables('publicIPAddressName')]"
location: "[parameters('location')]"
properties:
publicIPAddressVersion: "IPv4"
publicIPAllocationMethod: "[variables('publicIPAddressType')]"
idleTimeoutInMinutes: 4
sku:
name: "[variables('publicIpAddressSku')]"
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
publicIPAddress:
id: "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
subnet:
id: "[variables('subnetRef')]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
# todo - tags to be passed by cloudforms from the options hash
CFManaged: "true"
Owner: ucats
OwnerMail: ucats@nottingham.ac.uk
CFRequestID: "1234"
CFServiceID: "1234"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: "[deployment().name]"
# cannot natively retrieve dynamic ip in the same deployment, need nested self referencing deployment to perform variable lookup (requiring uri to blob of this deployment template)
# ipAddress:
# type: string
# value: "[reference(resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))).IpAddress]"

View File

@ -0,0 +1,105 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "UK South",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"description": "Allow SSH traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 22,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "DenyVnetInBound",
"properties": {
"description": "isolate instances on the same range from each another",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Deny",
"priority": 3996,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
},
{
"name": "DenyVnetOutBound",
"properties": {
"description": "isolate instances on the same range from each another",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Deny",
"priority": 3996,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,77 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: UK South
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: SSH
properties:
description: Allow SSH traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 22
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
- name: DenyVnetInBound
properties:
description: isolate instances on the same range from each another
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: Deny
priority: 3996
direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
- name: DenyVnetOutBound
properties:
description: isolate instances on the same range from each another
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: Deny
priority: 3996
direction: Outbound

View File

@ -0,0 +1,105 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "UK South",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "RDP",
"properties": {
"description": "Allow RDP traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 3389,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "DenyVnetInBound",
"properties": {
"description": "isolate instances on the same range from each another",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Deny",
"priority": 3996,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
},
{
"name": "DenyVnetOutBound",
"properties": {
"description": "isolate instances on the same range from each another",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Deny",
"priority": 3996,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,77 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: UK South
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: RDP
properties:
description: Allow RDP traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 3389
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
- name: DenyVnetInBound
properties:
description: isolate instances on the same range from each another
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: Deny
priority: 3996
direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
- name: DenyVnetOutBound
properties:
description: isolate instances on the same range from each another
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: Deny
priority: 3996
direction: Outbound

View File

@ -0,0 +1,30 @@
## Default target Resource Group
These templates were built on the GBUoN-uks-Dev subscription in the UI-SPP-DEV-001 resource group.
## json vs yaml
The templates are written in yaml and converted to json with yarn. Conversion operates both ways, it is helpful to take example json arm templates and convert to yaml - usage
https://github.com/Azure/azure-quickstart-templates Yaml allows comments and is much easier to read. https://github.com/TeamYARM/YARM-CLI
./Yarm.ConsoleApp.exe -i spectrum_scale_ARM_v2.yaml
spectrum_scale_ARM_v2.yaml => spectrum_scale_ARM_v2.json
## Templates
CFInstance.json
CFInstance.yaml
CFLinux_nsg.json
CFLinux_nsg.yaml
CFWindows_nsg.json
CFWindows_nsg.yaml
spectrum_scale_ARM_v2.yaml
## Purpose
### CFinstance
Generic template for an Azure instance with attached network adapter and public ip, will use parameterized network security group.
### CFWindows/CFLinux
Templates for windows or linux network security group, allows RDP/SSH respectively. Host isolation rules in place.
### spectrum_scale_ARM_v2
Template for single node spectrum scale storage, includes own network security group and secondary IP (previously secondary network interface). Used to write Ansible API calls.

View File

@ -0,0 +1,238 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "RedHat:RHEL:7.7:7.7.2020020415",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table"
}
},
"adminUsername": {
"type": "string",
"defaultValue": "ocfadmin",
"metadata": {
"description": "User name for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"defaultValue": "mnBMghZLWg63Kge2",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B4ms",
"metadata": {
"description": "Virtual machine size."
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "UI-SPP-DEV-001-vnet",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "default",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "UK South",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
}
},
"variables": {
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"publicIPAddressName": "[concat(variables('resourcePrefix'), '-pip')]",
"publicIPAddressType": "Dynamic",
"publicIpAddressSku": "Basic",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupName": "[variables('resourcePrefix')]",
"subnetRef": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/publicIPAddresses",
"name": "[variables('publicIPAddressName')]",
"location": "[parameters('location')]",
"properties": {
"publicIPAddressVersion": "IPv4",
"publicIPAllocationMethod": "[variables('publicIPAddressType')]",
"idleTimeoutInMinutes": 4
},
"sku": {
"name": "[variables('publicIpAddressSku')]"
}
},
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[variables('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"description": "Allow SSH traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 22,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
}
]
}
},
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"primary": true,
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
},
{
"name": "ipconfig2",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
],
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
},
"enableIPForwarding": true
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "ucats",
"OwnerMail": "ucats@nottingham.ac.uk",
"CFRequestID": "spectrum_scale_test",
"CFServiceID": "spectrum_scale_test",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
}
}
}

View File

@ -0,0 +1,190 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: RedHat:RHEL:7.7:7.7.2020020415
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table
adminUsername:
type: string
defaultValue: ocfadmin
metadata:
description: User name for the Virtual Machine.
adminPassword:
type: securestring
defaultValue: mnBMghZLWg63Kge2
metadata:
description: Password for the Virtual Machine.
vmSize:
type: string
defaultValue: Standard_B4ms
metadata:
description: Virtual machine size.
virtualNetworkName:
type: string
defaultValue: UI-SPP-DEV-001-vnet
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: default
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: UK South
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
variables:
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
publicIPAddressName: "[concat(variables('resourcePrefix'), '-pip')]"
publicIPAddressType: Dynamic
publicIpAddressSku: Basic
osDiskType: StandardSSD_LRS
networkSecurityGroupName: "[variables('resourcePrefix')]"
subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/publicIPAddresses
name: "[variables('publicIPAddressName')]"
location: "[parameters('location')]"
properties:
publicIPAddressVersion: IPv4
publicIPAllocationMethod: "[variables('publicIPAddressType')]"
idleTimeoutInMinutes: 4
sku:
name: "[variables('publicIpAddressSku')]"
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[variables('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: SSH
properties:
description: Allow SSH traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 22
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
- "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
primary: true
privateIPAllocationMethod: Dynamic
publicIPAddress:
id: "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
subnet:
id: "[variables('subnetRef')]"
# this only allocates an ip in Azure, waagent not smart enough to bind to adapter
# used to reserve a floating ip that can be used for CES, enableIPForwarding: boolean required
- name: ipconfig2
properties:
privateIPAllocationMethod: Dynamic
subnet:
id: "[variables('subnetRef')]"
networkSecurityGroup:
id: "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
enableIPForwarding: true
# no longer using secondary interface for CES, using above reserved ip for ces
# - apiVersion: 2019-11-01
# type: Microsoft.Network/networkInterfaces
# name: "[concat(variables('nicName'), '1')]"
# location: "[parameters('location')]"
# dependsOn:
# - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
# - "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
# properties:
# ipConfigurations:
# - name: ipconfig1
# properties:
# privateIPAllocationMethod: Dynamic
# subnet:
# id: "[variables('subnetRef')]"
# networkSecurityGroup:
# id: "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: ucats
OwnerMail: ucats@nottingham.ac.uk
CFRequestID: spectrum_scale_test
CFServiceID: spectrum_scale_test
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
# no longer using secondary interface for CES
# - id: "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nicName'), '1'))]"
# properties:
# primary: false
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
deploymentName:
type: string
value: '[deployment().name]'
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
vmName:
type: string
value: "[variables('vmName')]"

View File

@ -0,0 +1,227 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "RedHat:RHEL:7.8:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "User name for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": 2.1,
"autoUpgradeMinorVersion": true,
"settings": {
"skipDos2Unix": false
},
"protectedSettings": {
"script": "H4sIAGczD18CA61XbXPbNhL+zl+xlZ2xPRFI2ZMmEzu6q8exm06duNM66fRyHg5EgBIsEGAAUIqT9n777QKUZDu6m/twXyhxAezLg91nlzvfFRNlign3syzbAR9sC97PIFiQxndOgrHQeelA26kyIDqnzBSEbTi+3Vp8PIWuFTzIIQjl+URLCDPlobYOhJx008zf+SCbKui1ekG2ljNpUDWp04sGVMOnEhgZA6418AVXOqrzLa8koKUPPwI3AuTnIPFnZhsJlx8yvegFTMPhaPTk4tfzcyiEXBTO2rCYFrRRL7LPtS+nzi6BiW3LFL0MEL5kQTWSAooey8Do/Ys1Es47Z1tZXFojrKEDMf6ERXbXNcDu0E0fyH0nuW4EBusFzN3ke7a0bo5LQVmTBFpNPFghbu2k/2HNnJwRyoHnzYSzyjaNNSxYqz1wUWmVZRUP8DcoZKgKUpNX1tTw6tX51QU6dIYvatq5ZMYb1bYyeGj4HUwktBqBFIRkvCG0gzFadwfcw1JqnSlT6U5EBx4ayEWRZR8xBTAHpjcZXmzNOx1gDBc/XZ4fFwvuClyNByiuHF8ymItq6w6Upw1cNMqUmF0LTLBvdsZVEXeibTXpjfqN/TKijCdPX+fvrq6vf3r345vTt/npWf7+Z9xkfKmtnXftel9wnXywkFxM4qCquQylVrWkK0f50bNZhjdp5PK+9IXIKLuX3ImYn6vzDvXiS821x7d2rowKJTfVzDq/jo5gxaUiaF9U0gV8cjbpjNAyr1zIYGcVW1XxaiZLw6PRn8//+BXDO26l8wrryYTjJ187Jf5CbGJ0BMsWGPDo19VNbEXp0R1s3UNGUponJG/oWG5sCJgOM97kvMq7+X86nf/veymLM4mIwd6pPxy9bN/88ex39dK+MPwfZuZ2vtuDPyECC+wDkNOqkuWZtp24sK7xP2xTeq9iUlXer5mPvApqIdm6GG6y/gIYlhvCjJ5SESMCvbmbjHcBwcB0YatqH8Od9LhlS6Q3WcMNUhtLNIhbjb2vQbCGty2xYFKCNd92QTrWX/zu/sz6EF+YP9gs2w4XbTc+/YIsvUGg/OXqbIjia+nJjaGoxlzQc+NXlFX07OYJ8Q1DOzzGXejpK0t1E2luG7LIOJ86iQTSyMCRMjkEPqXeUWPhgl0a6R7z4iyE1h8XhdB5LYV1vHX2FpHPrZsWbTcpZCt1fDAnteReMo1c7AN7gdByV81y1zaPtd5+yqK18e5+1TmNSL2Bt71Px1SdMCDDaPfw+cv86Ptnef9brDwvoipTyb/zVrEFVZk146PR4Us2es5GhwNMvNtPkPf45xioR5FHPt3zxT8HRTGl3OTLObALgMHJAPa+Ikvsq/HhiXo1fndxop4+PVD1/q76V3FFzh4XB9jbtAooGiLwmHPDwfHg4KTFHktXQJKPRzfw195Bpmr4COwLDHZjoAO4OUEWl6aPu1GeOil6ldUPmwQlb3w8yHoSYKbHmiZy2pK4WRWbSVkrLcseEOLErK8DOma8H0LLmzU/FFsrgIuynxi2W6Km8F+5PKVjGUEfx3yUgi1VmLHUFRNVVk4KrFiFBIy7romUlSgxwRZKRHLjIpnCOcTJsuXeY1cWpapLW9cala6OrTjYz2Ss7s2QpAVvS8+9LrGGZ0p8U6G7aQsa3tR11Il9QU94NS9XPR7V0t/iSffDE0GUMHvk6w501MVbWalaVUDxwqr+gVeV7TBN7ITKJy3irIY9KMMRqqw7re/KTx3HxqWkiG0kdqHYnaTpGolTglxLki3NHQ5hm8EAuw3mcEN1AbalocJnamoIPRylsH82splIt4Yb0Xd3ZboN6pa2I/58PhqtYpFUwYBI4pTYGUKHIWeg0QBX7+PM+MiFhCZNoGU6XMbDY9w+fo9SP6Q/RsUEDXfD12fEd/i8x3ckq+iJqbYT9SXn/y8KV/6Vwc6liYr9PVSJUJ3CC8L7kt4TOSZeTO/3b9yrptUySz8lZotdxsBJXar6xNbrMR1HS8wApfsBnFhXYcaKbyduf18mTRqrY1eLU2+Hezph0dQD5kginP02off8kdyB08vL8T4+DuDd1S+nv/32++tjkvVurshZSD8nN2pnGyBmR1BaO1z54bDG+uTiaan/dmhxqJemUtgUke5ZoiOWuqkDxtbnO8VICXvB0hjDomilkmG/8I8bxuejqU2bVyvx5lbLb0+vz7P+9qgdrikvepaobPNVtKV/PoJ8I47fPrFnp+8YYlUcxS1WslZz/PTBvRP8MBkmWX+zVN1US6vVh99n/WSBBlSN44zPdvqY0ulsJx1Cw/IzTk5Th58BkR9iOEuOeJqQxbXRvwHIUlJJEQ4AAA=="
}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,165 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: RedHat:RHEL:7.8:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table
adminUsername:
type: string
metadata:
description: User name for the Virtual Machine.
adminPassword:
type: securestring
metadata:
description: Password for the Virtual Machine.
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
defaultValue: rg-vn-rem-we-1
#defaultValue: unused
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.Azure.Extensions
type: CustomScript
typeHandlerVersion: 2.1
autoUpgradeMinorVersion: true
settings:
skipDos2Unix: false
protectedSettings:
script: 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
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
#value: "[reference('nicName').ipConfigurations[0].properties.privateIPAddress]"
#value: "[reference(concat(variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,227 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "Canonical:UbuntuServer:18.04-LTS:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "User name for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": 2.1,
"autoUpgradeMinorVersion": true,
"settings": {
"skipDos2Unix": false
},
"protectedSettings": {
"script": "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"
}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,165 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: Canonical:UbuntuServer:18.04-LTS:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table
adminUsername:
type: string
metadata:
description: User name for the Virtual Machine.
adminPassword:
type: securestring
metadata:
description: Password for the Virtual Machine.
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
defaultValue: rg-vn-rem-we-1
#defaultValue: unused
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.Azure.Extensions
type: CustomScript
typeHandlerVersion: 2.1
autoUpgradeMinorVersion: true
settings:
skipDos2Unix: false
protectedSettings:
script: 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
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
#value: "[reference('nicName').ipConfigurations[0].properties.privateIPAddress]"
#value: "[reference(concat(variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,285 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "User name for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region"
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
},
"domainUsername": {
"type": "string",
"defaultValue": "service_CloudForms",
"metadata": {
"description": "Username of the account on the domain"
}
},
"domainPassword": {
"type": "securestring",
"defaultValue": "As109pHY4Wi9o7naZnhr#!",
"metadata": {
"description": "Password of the account on the domain"
}
},
"ouPath": {
"type": "string",
"defaultValue": "OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk",
"metadata": {
"description": "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
}
},
"domainJoinOptions": {
"type": "int",
"defaultValue": 3,
"metadata": {
"description": "Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
"rdpgroupsCmd": "[concat('Add-LocalGroupMember -Group ''Remote Desktop Users'' -Member', ' ', variables('owner'))]",
"localadminCommand": "[concat('Add-LocalGroupMember -Group Administrators -Member', ' ', variables('owner'))]",
"powershellCmd": "[concat('powershell.exe -ExecutionPolicy Unrestricted', ' ', variables('rdpgroupsCmd'), ';', variables('localadminCommand'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2015-06-15",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'joindomain')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"tags": {
"domain": "[parameters('domainToJoin')]"
},
"properties": {
"publisher": "Microsoft.Compute",
"type": "JsonADDomainExtension",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {
"Name": "[parameters('domainToJoin')]",
"OUPath": "[parameters('ouPath')]",
"User": "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]",
"Restart": true,
"Options": "[parameters('domainJoinOptions')]"
},
"protectedSettings": {
"Password": "[parameters('domainPassword')]"
}
}
},
{
"apiVersion": "2018-06-01",
"condition": "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/rdpgroups')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
],
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": 1.1,
"autoUpgradeMinorVersion": true,
"settings": "",
"protectedSettings": {
"commandToexecute": "[variables('powershellCmd')]"
}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,213 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: "MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest"
metadata:
description: "az vm image list --output table / az vm image list -p RedHat --all --output table"
adminUsername:
type: string
metadata:
description: User name for the Virtual Machine.
adminPassword:
type: securestring
metadata:
description: Password for the Virtual Machine.
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region
utcValue:
type: string
defaultValue: "[utcNow()]"
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
domainUsername:
type: string
defaultValue: service_CloudForms
metadata:
description: Username of the account on the domain
domainPassword:
type: securestring
defaultValue: As109pHY4Wi9o7naZnhr#!
metadata:
description: Password of the account on the domain
ouPath:
type: string
defaultValue: OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk
metadata:
description: "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
domainJoinOptions:
type: int
defaultValue: 3
metadata:
description: Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
rdpgroupsCmd: "[concat('Add-LocalGroupMember -Group ''Remote Desktop Users'' -Member', ' ', variables('owner'))]"
localadminCommand: "[concat('Add-LocalGroupMember -Group Administrators -Member', ' ', variables('owner'))]"
powershellCmd: "[concat('powershell.exe -ExecutionPolicy Unrestricted', ' ', variables('rdpgroupsCmd'), ';', variables('localadminCommand'))]"
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2015-06-15
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'joindomain')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
tags:
domain: "[parameters('domainToJoin')]"
properties:
publisher: Microsoft.Compute
type: JsonADDomainExtension
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings:
Name: "[parameters('domainToJoin')]"
OUPath: "[parameters('ouPath')]"
User: "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]"
Restart: true
Options: "[parameters('domainJoinOptions')]"
protectedSettings:
Password: "[parameters('domainPassword')]"
- apiVersion: 2018-06-01
# conditionally run if owner is a valid domain user and not a placeholder from this template or cloudforms admin user without valid uon email/account
# condition: "[or(not(equals(variables('owner'), 'unused')), not(equals(variables('owner'), 'donotreply')))]" # wont match second condition
condition: "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/rdpgroups')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
properties:
publisher: Microsoft.Compute
type: CustomScriptExtension
typeHandlerVersion: 1.10
autoUpgradeMinorVersion: true
settings:
protectedSettings:
commandToexecute: "[variables('powershellCmd')]"
# example for adding host entry when using default Azure dns
# commandToExecute: powershell.exe -ExecutionPolicy Unrestricted Add-Content -Path "$env:windir\System32\drivers\etc\hosts" -Value "`r`n10.102.1.6`tad.nottingham.ac.uk" -Force
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: "[deployment().name]"
ipAddress:
type: string
#value: "[reference('nicName').ipConfigurations[0].properties.privateIPAddress]"
#value: "[reference(concat(variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,77 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"description": "Allow SSH traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 22,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,79 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: SSH
properties:
description: Allow SSH traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 22
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetInBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetOutBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Outbound

View File

@ -0,0 +1,77 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "RDP",
"properties": {
"description": "Allow RDP traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 3389,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,79 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: RDP
properties:
description: Allow RDP traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 3389
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetInBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetOutBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Outbound

View File

@ -0,0 +1,49 @@
## Default target Resource Group
These templates were built on the Research Managed subscription in the rg-svc-rem-we-spp-1 resource group, the network vnet+subnet used resuide within the rg-vn-rem-we-1 resource group.
## json vs yaml
The templates are written in yaml and converted to json with yarn. Conversion operates both ways, it is helpful to take example json arm templates and convert to yaml - usage
https://github.com/Azure/azure-quickstart-templates Yaml allows comments and is much easier to read. https://github.com/TeamYARM/YARM-CLI
./Yarm.ConsoleApp.exe -i CFInstance_win.yaml
CFInstance_win.yaml => CFInstance_win.json
## Templates
CFInstance_rhel.json
CFInstance_rhel.yaml
CFInstance_win.json
CFInstance_win.yaml
CFLinux_nsg.json
CFLinux_nsg.yaml
CFWindows_nsg.json
CFWindows_nsg.yaml
rhel_customscript_extension.sh
## Purpose
### Azure_RHEL_instance / Azure_UbuntuServer_instance
Template for a RHEL Azure instance with attached network adapter and dynamic private ip, uses parameterized network security group CFLinux_nsg.
Parameter networkResourceGroup included for UoN midtier vnet that resides in a different resource group, if the value is 'unused' network interfaces will be build in the vnet+subnet of the resource group supplied during the invocation of the template.
Uses waagent to run a CustomScript extension rather than cloud_init, this joins the host to the domain and performs a lookup of the owner tag to modify the sssd.conf with the tag value to ensure only the owner has access to the instance.
The customscript is base64 encoded and the resultant string is put in the ARM template.
#### create customscript extension script property
https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-linux
cat rhel_customscript_extension.sh | gzip -9 | base64 -w 0
#### customscript extensions
rhel_customscript_extension.sh
ubuntu_customscript_extension.sh
### Azure_WindowsServer_instance
Template for a Windows Azure instance with attached network adapter and dynamic private ip, uses parameterized network security group CFWindows_nsg.
Parameter networkResourceGroup included for UoN midtier vnet that resides in a different resource group, if the value is 'unused' network interfaces will be build in the vnet+subnet of the resource group supplied during the invocation of the template.
Uses waagent to run JsonADDomainExtension extension and CustomScriptExtension extension to join a domain and chnage the local rdp group to ensure only the owner has access to the instance.
### CFLinux_Azure_network_security_group / CFWindows_Azure_network_security_group
Templates for windows or linux network security group, allows RDP/SSH respectively. Host isolation rules dropped owing to gateway being in the default vnet range.

View File

@ -0,0 +1,129 @@
#!/bin/bash
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# when using lvm image - use all available space in VG and extend home LV
lvextend -l 100%FREE /dev/rootvg/homelv
xfs_grow -d /dev/rootvg/homelv
# set tz
timedatectl set-timezone Europe/London
# join domain
yum -y install realmd sssd krb5-workstation krb5-libs oddjob oddjob-mkhomedir samba-common-tools adcli
cat > /etc/krb5.conf <<EOF
# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK
# query metadata tag to find owner
yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install jq
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop from epel repo, enable rhel optional repo for dependencies
yum-config-manager --enable rhui-rhel-7-server-rhui-optional-rpms
yum -y install x2goserver
yum -y groupinstall MATE
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#yum -y update
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,142 @@
#!/bin/bash
# ubuntu much prefers cloud-init
# should be rewritten with a common function to wait for apt db and metadata availability with timeouts
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# set tz
timedatectl set-timezone Europe/London
# join domain
# wait loop until apt database is not locked to avoid clash with Azure policy installs, add a little delay to be able to contact apt repos (probably not Azure ones?)
aptupdate=1
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
until [ $aptupdate -eq 0 ]
do
apt-get -y update
apt-get -y install jq
which jq
aptupdate=$?
sleep 5
done
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y install krb5-user samba sssd sssd-tools libnss-sss libpam-sss realmd adcli expect
cat > /etc/krb5.conf <<EOF
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
# password cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# if the password changes this script must be reprocessed and the ARM template updated in the CloudForms orchestration template
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK --install=/
# owner cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# query metadata tag to find owner, this will be used in the sssd.conf whitelist and sudoers
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop, mate desktop meta package requires user interaction to select window manager, expect doesnt play well in whatever shell this script is run from - this minimal install is quicker @10mins
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y --no-install-recommends install x2goserver firefox caja compiz-mate engrampa eom folder-color-caja gnome-accessibility-themes gnome-colors-common gnome-icon-theme gnome-orca grub2-themes-ubuntu-mate indicator-messages indicator-power indicator-session indicator-sound lightdm-gtk-greeter mate-core mate-accessibility-profiles mate-applet-appmenu mate-applet-brisk-menu mate-calc mate-desktop mate-dock-applet mate-hud mate-icon-theme mate-menu mate-menus mate-netbook mate-optimus mate-screensaver mate-screensaver-common mate-system-monitor mate-tweak mate-user-guide mate-utils mate-window-applets-common mate-window-buttons-applet mate-window-menu-applet mate-window-title-applet plank plymouth-theme-ubuntu-mate-logo plymouth-theme-ubuntu-mate-text sessioninstaller sound-theme-freedesktop tilda ubuntu-mate-artwork ubuntu-mate-core ubuntu-mate-default-settings ubuntu-mate-guide ubuntu-mate-icon-themes ubuntu-mate-lightdm-theme ubuntu-mate-themes ubuntu-mate-wallpapers* ubuntu-standard
# enable home directory creation at logon - perform after desktop install to avoid manual prompts with desktop install
sed -i 's/^.*pam_sss.so.*$/&\nsession required\tpam_mkhomedir.so skel=\/etc\/skel\/ umask=0077/' /etc/pam.d/common-session
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#apt-get -y upgrade
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,311 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "Canonical:UbuntuServer:18.04-LTS:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "UI-SPP-DEV-001-vnet",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "default",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "UK South",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"publicIPAddressName": "[concat(variables('resourcePrefix'), '-pip')]",
"publicIPAddressType": "Dynamic",
"publicIpAddressSku": "Basic",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/publicIPAddresses",
"name": "[variables('publicIPAddressName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"publicIPAddressVersion": "IPv4",
"publicIPAllocationMethod": "[variables('publicIPAddressType')]",
"idleTimeoutInMinutes": 4
},
"sku": {
"name": "[variables('publicIpAddressSku')]"
}
},
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "linux",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,255 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: Canonical:UbuntuServer:18.04-LTS:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
defaultValue: unused
#defaultValue: rg-vn-rem-we-1 # only used in the PROD resource group
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance
virtualNetworkName:
type: string
defaultValue: UI-SPP-DEV-001-vnet
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: default
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: UK South
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
# no access to domain in DEV resource group
# domainToJoin:
# type: string
# defaultValue: ad.nottingham.ac.uk
# metadata:
# description: The FQDN of the AD domain
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
publicIPAddressName: "[concat(variables('resourcePrefix'), '-pip')]"
publicIPAddressType: Dynamic
publicIpAddressSku: Basic
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
resources:
# public ip only available in DEV resource group
- apiVersion: 2019-11-01
type: Microsoft.Network/publicIPAddresses
name: "[variables('publicIPAddressName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
properties:
publicIPAddressVersion: "IPv4"
publicIPAllocationMethod: "[variables('publicIPAddressType')]"
idleTimeoutInMinutes: 4
sku:
name: "[variables('publicIpAddressSku')]"
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: linux
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
publicIPAddress:
id: "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
# no access to domain in DEV resource group
# - apiVersion: 2019-12-01
# type: Microsoft.Compute/virtualMachines/extensions
# name: "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]"
# location: "[parameters('location')]"
# tags:
# CFManaged: true
# Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
# OwnerMail: "[parameters('email')]"
# ADDomain: "[parameters('domainToJoin')]"
# Cost Center: "[parameters('projectCode')]"
# dependsOn:
# - "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
# properties:
# publisher: Microsoft.Azure.Extensions
# type: CustomScript
# typeHandlerVersion: 2.1
# autoUpgradeMinorVersion: true
# settings:
# skipDos2Unix: false
# protectedSettings:
# script: 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
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,138 @@
#!/bin/bash
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# when using lvm image - use all available space in VG and extend home LV
lvextend -l 100%FREE /dev/rootvg/homelv
xfs_grow -d /dev/rootvg/homelv
# add secondary disk
parted -s /dev/sdc -- mklabel gpt mkpart primary xfs 0% 100%
mkfs.xfs /dev/sdc1
xfs_admin -L uondata /dev/sdc1
mkdir /uondata
echo "LABEL=uondata /uondata xfs defaults 0 0" >> /etc/fstab
mount -a
chmod 777 /uondata
# set tz
timedatectl set-timezone Europe/London
# join domain
yum -y install realmd sssd krb5-workstation krb5-libs oddjob oddjob-mkhomedir samba-common-tools adcli
cat > /etc/krb5.conf <<EOF
# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK
# query metadata tag to find owner
yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install jq
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop from epel repo, enable rhel optional repo for dependencies
yum-config-manager --enable rhui-rhel-7-server-rhui-optional-rpms
yum -y install x2goserver
yum -y groupinstall MATE
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#yum -y update
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,341 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "RedHat:RHEL:7.7:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example RedHat:RHEL:7.7:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "linux",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": 2.1,
"autoUpgradeMinorVersion": true,
"settings": {
"skipDos2Unix": false
},
"protectedSettings": {
"script": "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"
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
},
{
"apiVersion": "2015-06-15",
"condition": "[contains(parameters('vmSize'), 'Standard_N')]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/nvidia')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/join_domain_install_desktop')]"
],
"properties": {
"publisher": "Microsoft.HpcCompute",
"type": "NvidiaGpuDriverLinux",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,252 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: RedHat:RHEL:7.7:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example RedHat:RHEL:7.7:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
#defaultValue: unused
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: linux
Cost Center: "[parameters('projectCode')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.Azure.Extensions
type: CustomScript
typeHandlerVersion: 2.1
autoUpgradeMinorVersion: true
settings:
skipDos2Unix: false
protectedSettings:
script: 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
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
- apiVersion: 2015-06-15
# supports up to Ubuntu 18.04 LTS and RHEL 7.7, will fight join_domain_install_desktop for apt lock without dependency
# https://docs.microsoft.com/en-us/azure/virtual-machines/linux/n-series-driver-setup
# https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/hpccompute-gpu-linux
condition: "[contains(parameters('vmSize'), 'Standard_N')]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/nvidia')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/join_domain_install_desktop')]"
properties:
publisher: Microsoft.HpcCompute
type: NvidiaGpuDriverLinux
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings: {}
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,151 @@
#!/bin/bash
# ubuntu much prefers cloud-init
# should be rewritten with a common function to wait for apt db and metadata availability with timeouts
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# add secondary disk
parted -s /dev/sdc -- mklabel gpt mkpart primary xfs 0% 100%
mkfs.xfs /dev/sdc1
xfs_admin -L uondata /dev/sdc1
mkdir /uondata
echo "LABEL=uondata /uondata xfs defaults 0 0" >> /etc/fstab
mount -a
chmod 777 /uondata
# set tz
timedatectl set-timezone Europe/London
# join domain
# wait loop until apt database is not locked to avoid clash with Azure policy installs, add a little delay to be able to contact apt repos (probably not Azure ones?)
aptupdate=1
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
until [ $aptupdate -eq 0 ]
do
apt-get -y update
apt-get -y install jq
which jq
aptupdate=$?
sleep 5
done
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y install krb5-user samba sssd sssd-tools libnss-sss libpam-sss realmd adcli expect
cat > /etc/krb5.conf <<EOF
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
# password cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# if the password changes this script must be reprocessed and the ARM template updated in the CloudForms orchestration template
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK --install=/
# owner cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# query metadata tag to find owner, this will be used in the sssd.conf whitelist and sudoers
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop, mate desktop meta package requires user interaction to select window manager, expect doesnt play well in whatever shell this script is run from - this minimal install is quicker @10mins
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y --no-install-recommends install x2goserver firefox caja compiz-mate engrampa eom folder-color-caja gnome-accessibility-themes gnome-colors-common gnome-icon-theme gnome-orca grub2-themes-ubuntu-mate indicator-messages indicator-power indicator-session indicator-sound lightdm-gtk-greeter mate-core mate-accessibility-profiles mate-applet-appmenu mate-applet-brisk-menu mate-calc mate-desktop mate-dock-applet mate-hud mate-icon-theme mate-menu mate-menus mate-netbook mate-optimus mate-screensaver mate-screensaver-common mate-system-monitor mate-tweak mate-user-guide mate-utils mate-window-applets-common mate-window-buttons-applet mate-window-menu-applet mate-window-title-applet plank plymouth-theme-ubuntu-mate-logo plymouth-theme-ubuntu-mate-text sessioninstaller sound-theme-freedesktop tilda ubuntu-mate-artwork ubuntu-mate-core ubuntu-mate-default-settings ubuntu-mate-guide ubuntu-mate-icon-themes ubuntu-mate-lightdm-theme ubuntu-mate-themes ubuntu-mate-wallpapers* ubuntu-standard
# enable home directory creation at logon - perform after desktop install to avoid manual prompts with desktop install
sed -i 's/^.*pam_sss.so.*$/&\nsession required\tpam_mkhomedir.so skel=\/etc\/skel\/ umask=0077/' /etc/pam.d/common-session
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#apt-get -y upgrade
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,341 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "Canonical:UbuntuServer:18.04-LTS:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "linux",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": 2.1,
"autoUpgradeMinorVersion": true,
"settings": {
"skipDos2Unix": false
},
"protectedSettings": {
"script": "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"
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
},
{
"apiVersion": "2015-06-15",
"condition": "[contains(parameters('vmSize'), 'Standard_N')]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/nvidia')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/join_domain_install_desktop')]"
],
"properties": {
"publisher": "Microsoft.HpcCompute",
"type": "NvidiaGpuDriverLinux",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,252 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: Canonical:UbuntuServer:18.04-LTS:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
#defaultValue: unused
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: linux
Cost Center: "[parameters('projectCode')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'join_domain_install_desktop')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.Azure.Extensions
type: CustomScript
typeHandlerVersion: 2.1
autoUpgradeMinorVersion: true
settings:
skipDos2Unix: false
protectedSettings:
script: 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
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
- apiVersion: 2015-06-15
# supports up to Ubuntu 18.04 LTS and RHEL 7.7, will fight join_domain_install_desktop for apt lock without dependency
# https://docs.microsoft.com/en-us/azure/virtual-machines/linux/n-series-driver-setup
# https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/hpccompute-gpu-linux
condition: "[contains(parameters('vmSize'), 'Standard_N')]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/nvidia')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/join_domain_install_desktop')]"
properties:
publisher: Microsoft.HpcCompute
type: NvidiaGpuDriverLinux
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings: {}
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,401 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region"
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
},
"domainUsername": {
"type": "string",
"defaultValue": "service_CloudForms",
"metadata": {
"description": "Username of the account on the domain"
}
},
"domainPassword": {
"type": "securestring",
"defaultValue": "As109pHY4Wi9o7naZnhr#!",
"metadata": {
"description": "Password of the account on the domain"
}
},
"ouPath": {
"type": "string",
"defaultValue": "OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk",
"metadata": {
"description": "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
}
},
"domainJoinOptions": {
"type": "int",
"defaultValue": 3,
"metadata": {
"description": "Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
"rdpgroupsCmd": "[concat('Add-LocalGroupMember -Group ''Remote Desktop Users'' -Member', ' ', variables('owner'))]",
"localadminCmd": "[concat('Add-LocalGroupMember -Group Administrators -Member', ' ', variables('owner'))]",
"disablePopupCmd": "[concat('\"Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask\"')]",
"datadiskCmd": "[concat('\"Get-Disk | Where partitionstyle -eq ''raw'' | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel ''data'' -Confirm:$false\"')]",
"installfirefoxCmd": "\"[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));choco install -y firefox googlechrome\"",
"powershellCmd": "[concat('powershell.exe -ExecutionPolicy Unrestricted', ' ', variables('rdpgroupsCmd'), ';', variables('localadminCmd'), ';', variables('disablePopupCmd'), ';', variables('datadiskCmd'), ';', variables('installfirefoxCmd'))]"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "windows",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]",
"windowsConfiguration": {
"timeZone": "GMT Standard Time"
}
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2015-06-15",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'joindomain')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"publisher": "Microsoft.Compute",
"type": "JsonADDomainExtension",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {
"Name": "[parameters('domainToJoin')]",
"OUPath": "[parameters('ouPath')]",
"User": "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]",
"Restart": true,
"Options": "[parameters('domainJoinOptions')]"
},
"protectedSettings": {
"Password": "[parameters('domainPassword')]"
}
}
},
{
"apiVersion": "2018-06-01",
"condition": "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/rdpgroups')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
],
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": 1.1,
"autoUpgradeMinorVersion": true,
"settings": "",
"protectedSettings": {
"commandToexecute": "[variables('powershellCmd')]"
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
},
{
"apiVersion": "2015-06-15",
"condition": "[contains(parameters('vmSize'), 'Standard_N')]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/nvidia')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.HpcCompute",
"type": "NvidiaGpuDriverWindows",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,304 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
#defaultValue: unused
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region
utcValue:
type: string
defaultValue: "[utcNow()]"
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
domainUsername:
type: string
defaultValue: service_CloudForms
metadata:
description: Username of the account on the domain
domainPassword:
type: securestring
defaultValue: As109pHY4Wi9o7naZnhr#!
metadata:
description: Password of the account on the domain
ouPath:
type: string
defaultValue: OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk
metadata:
description: "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
domainJoinOptions:
type: int
defaultValue: 3
metadata:
description: Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
rdpgroupsCmd: "[concat('Add-LocalGroupMember -Group ''Remote Desktop Users'' -Member', ' ', variables('owner'))]"
localadminCmd: "[concat('Add-LocalGroupMember -Group Administrators -Member', ' ', variables('owner'))]"
disablePopupCmd: "[concat('\"Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask\"')]"
datadiskCmd: "[concat('\"Get-Disk | Where partitionstyle -eq ''raw'' | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel ''data'' -Confirm:$false\"')]"
#setlocaleCmd: "[concat('Set-WinSystemLocale en-GB;Set-WinUserLanguageList -LanguageList en-GB -Force;Set-Culture -CultureInfo en-GB;Set-WinHomeLocation -GeoId 242;Set-TimeZone -Name \"GMT Standard Time\"')]" # setting the timezone in the osProfile sets these
installfirefoxCmd: "\"[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));choco install -y firefox googlechrome\""
powershellCmd: "[concat('powershell.exe -ExecutionPolicy Unrestricted', ' ', variables('rdpgroupsCmd'), ';', variables('localadminCmd'), ';', variables('disablePopupCmd'), ';', variables('datadiskCmd'), ';', variables('installfirefoxCmd'))]"
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: windows
Cost Center: "[parameters('projectCode')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
windowsConfiguration:
timeZone: GMT Standard Time
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2015-06-15
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'joindomain')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
properties:
publisher: Microsoft.Compute
type: JsonADDomainExtension
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings:
Name: "[parameters('domainToJoin')]"
OUPath: "[parameters('ouPath')]"
User: "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]"
Restart: true
Options: "[parameters('domainJoinOptions')]"
protectedSettings:
Password: "[parameters('domainPassword')]"
- apiVersion: 2018-06-01
# conditionally run if owner is a valid domain user and not a placeholder from this template or cloudforms admin user without valid uon email/account
# condition: "[or(not(equals(variables('owner'), 'unused')), not(equals(variables('owner'), 'donotreply')))]" # wont match second condition
condition: "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/rdpgroups')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
properties:
publisher: Microsoft.Compute
type: CustomScriptExtension
typeHandlerVersion: 1.10
autoUpgradeMinorVersion: true
settings:
protectedSettings:
commandToexecute: "[variables('powershellCmd')]"
# example for adding host entry when using default Azure dns
# commandToExecute: powershell.exe -ExecutionPolicy Unrestricted Add-Content -Path "$env:windir\System32\drivers\etc\hosts" -Value "`r`n10.102.1.6`tad.nottingham.ac.uk" -Force
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
- apiVersion: 2015-06-15
# seems to install driver on MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest if there are issues official support is for 2012 / 2016
# https://docs.microsoft.com/en-us/azure/virtual-machines/windows/n-series-driver-setup
# https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/hpccompute-gpu-windows
condition: "[contains(parameters('vmSize'), 'Standard_N')]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/nvidia')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.HpcCompute
type: NvidiaGpuDriverWindows
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings: {}
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: "[deployment().name]"
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,77 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"description": "Allow SSH traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 22,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,79 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: SSH
properties:
description: Allow SSH traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 22
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetInBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetOutBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Outbound

View File

@ -0,0 +1,77 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.1",
"parameters": {
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "Name of the network security group"
}
}
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[parameters('networkSecurityGroupName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": "true"
},
"properties": {
"securityRules": [
{
"name": "RDP",
"properties": {
"description": "Allow RDP traffic from anywhere",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": 3389,
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 100,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerInBound",
"properties": {
"description": "allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "AzureLoadBalancer",
"destinationAddressPrefix": "VirtualNetwork",
"access": "Allow",
"priority": 3995,
"direction": "Inbound"
}
},
{
"name": "AllowAzureLoadBalancerOutbound",
"properties": {
"description": "allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services",
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRange": "*",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "AzureLoadBalancer",
"access": "Allow",
"priority": 3995,
"direction": "Outbound"
}
}
]
}
}
]
}

View File

@ -0,0 +1,79 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.1
parameters:
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: Name of the network security group
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkSecurityGroups
name: "[parameters('networkSecurityGroupName')]"
location: "[parameters('location')]"
tags:
CFManaged: "true"
properties:
securityRules:
- name: RDP
properties:
description: Allow RDP traffic from anywhere
protocol: Tcp
sourcePortRange: '*'
destinationPortRange: 3389
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: Allow
priority: 100
direction: Inbound
- name: AllowAzureLoadBalancerInBound
properties:
description: allow essential Azure services from 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
destinationAddressPrefix: 'VirtualNetwork'
access: Allow
priority: 3995
direction: Inbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetInBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Inbound
- name: AllowAzureLoadBalancerOutbound
properties:
description: allow instances to essential Azure services 168.63.129.16, AzureLoadBalancer tag includes these services
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'AzureLoadBalancer'
access: Allow
priority: 3995
direction: Outbound
# entry removed, we now use uon prod midtier vnet with routing on the same subnet range, this causes access issues
# - name: DenyVnetOutBound
# properties:
# description: isolate instances on the same range from each another
# protocol: '*'
# sourcePortRange: '*'
# destinationPortRange: '*'
# sourceAddressPrefix: 'VirtualNetwork'
# destinationAddressPrefix: 'VirtualNetwork'
# access: Deny
# priority: 3996
# direction: Outbound

View File

@ -0,0 +1,184 @@
#!/bin/bash
exec 1> /root/$0.log 2>&1
set -x
# ubuntu much prefers cloud-init
# should be rewritten with a common function to wait for apt db and metadata availability with timeouts
# Let the system init correctly
sleep 20
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# set tz
timedatectl set-timezone Europe/London
# join domain
# wait loop until apt database is not locked to avoid clash with Azure policy installs, add a little delay to be able to contact apt repos (probably not Azure ones?)
aptupdate=1
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
until [ $aptupdate -eq 0 ];
do
apt-get -y update
apt-get -y install jq
which jq
aptupdate=$?
sleep 5
done
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y install krb5-user samba sssd sssd-tools libnss-sss libpam-sss realmd adcli expect
cat > /etc/krb5.conf <<EOF
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
# password cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# if the password changes this script must be reprocessed and the ARM template updated in the CloudForms orchestration template
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK --install=/
# owner cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# query metadata tag to find owner, this will be used in the sssd.conf whitelist and sudoers
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop, mate desktop meta package requires user interaction to select window manager, expect doesnt play well in whatever shell this script is run from - this minimal install is quicker @10mins
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y --no-install-recommends install x2goserver firefox caja compiz-mate engrampa eom folder-color-caja gnome-accessibility-themes gnome-colors-common gnome-icon-theme gnome-orca grub2-themes-ubuntu-mate indicator-messages indicator-power indicator-session indicator-sound lightdm-gtk-greeter mate-core mate-accessibility-profiles mate-applet-appmenu mate-applet-brisk-menu mate-calc mate-desktop mate-dock-applet mate-hud mate-icon-theme mate-menu mate-menus mate-netbook mate-optimus mate-screensaver mate-screensaver-common mate-system-monitor mate-tweak mate-user-guide mate-utils mate-window-applets-common mate-window-buttons-applet mate-window-menu-applet mate-window-title-applet plank plymouth-theme-ubuntu-mate-logo plymouth-theme-ubuntu-mate-text sessioninstaller sound-theme-freedesktop tilda ubuntu-mate-artwork ubuntu-mate-core ubuntu-mate-default-settings ubuntu-mate-guide ubuntu-mate-icon-themes ubuntu-mate-lightdm-theme ubuntu-mate-themes ubuntu-mate-wallpapers* ubuntu-standard
# enable home directory creation at logon - perform after desktop install to avoid manual prompts with desktop install
sed -i 's/^.*pam_sss.so.*$/&\nsession required\tpam_mkhomedir.so skel=\/etc\/skel\/ umask=0077/' /etc/pam.d/common-session
# add secondary disk
parted -s /dev/disk/azure/scsi1/lun0 -- mklabel gpt mkpart primary xfs 0% 100%
sleep 2 # allow partition to register
mkfs.xfs /dev/disk/azure/scsi1/lun0-part1
sleep 2 # allow filesystem creation to finish
xfs_admin -L uondata /dev/disk/azure/scsi1/lun0-part1
mkdir /uondata
echo "LABEL=uondata /uondata xfs defaults 0 0" >> /etc/fstab
mount -a
chmod 777 /uondata
# install carbon black
wget -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy_v10.tar.gz --strip-components=1
mv azcopy /usr/local/bin/
chmod +x /usr/local/bin/azcopy
rm -Rf azcopy*
mkdir /root/uon_packages
identretry=5 # loop with timeout until managed identity is available
msident=1
until [ $msident -eq 0 ] || [ $identretry -eq 0 ];
do
identretry=$((identretry-1))
azcopy login --identity
msident=$?
sleep 5
done
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/CarbonBlackClientSetup-linux-v7.0.0.14291.tgz' '/root/uon_packages'
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/CarbonBlackClientSetup-linux-v7.0.0.14291.sh' '/root/uon_packages'
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/sensorsettings.ini' '/root/uon_packages'
chmod +x /root/uon_packages/CarbonBlackClientSetup-linux-v7.0.0.14291.sh
cd /root/uon_packages
#while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done # installer script builds apt packages that can get blocked by the nvidia extension
./CarbonBlackClientSetup-linux-v7.0.0.14291.sh
cd /root
rm -Rf /root/uon_packages
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#apt-get -y upgrade
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,354 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "Canonical:UbuntuServer:18.04-LTS:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFLinux",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region."
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
"identityName": "extensionartefact"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "linux",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"[resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName'))]": {}
}
},
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'customscript')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": 2.1,
"autoUpgradeMinorVersion": true,
"settings": {
"skipDos2Unix": false
},
"protectedSettings": {
"fileUris": [
"https://extensionartefact.blob.core.windows.net/extensionartefact/Azure_UbuntuServer_customscript.sh"
],
"commandToExecute": "sudo sh Azure_UbuntuServer_customscript.sh",
"managedIdentity": {
"clientid": "[reference(resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName')), '2018-11-30', 'full').properties.clientId]"
}
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
},
{
"apiVersion": "2015-06-15",
"condition": "[contains(parameters('vmSize'), 'Standard_N')]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/nvidia')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/customscript')]"
],
"properties": {
"publisher": "Microsoft.HpcCompute",
"type": "NvidiaGpuDriverLinux",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"owner": {
"type": "string",
"value": "[variables('owner')]"
},
"email": {
"type": "string",
"value": "[parameters('email')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,262 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: Canonical:UbuntuServer:18.04-LTS:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example Canonical:UbuntuServer:18.04-LTS:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFLinux
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
#defaultValue: unused
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region.
utcValue:
type: string
defaultValue: '[utcNow()]'
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
identityName: extensionartefact
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: linux
Cost Center: "[parameters('projectCode')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
identity:
type: UserAssigned
userAssignedIdentities:
#"[resourceID('Microsoft.ManagedIdentity/userAssignedIdentities/',variables('identityName'))]": {}
"[resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName'))]": {}
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'customscript')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
publisher: Microsoft.Azure.Extensions
type: CustomScript
typeHandlerVersion: 2.1
autoUpgradeMinorVersion: true
settings:
skipDos2Unix: false
protectedSettings:
fileUris:
- https://extensionartefact.blob.core.windows.net/extensionartefact/Azure_UbuntuServer_customscript.sh
commandToExecute: "sudo sh Azure_UbuntuServer_customscript.sh"
managedIdentity:
clientid: "[reference(resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName')), '2018-11-30', 'full').properties.clientId]"
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
- apiVersion: 2015-06-15
# supports up to Ubuntu 18.04 LTS and RHEL 7.7, will fight customscript for apt lock without dependency
# https://docs.microsoft.com/en-us/azure/virtual-machines/linux/n-series-driver-setup
# https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/hpccompute-gpu-linux
condition: "[contains(parameters('vmSize'), 'Standard_N')]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/nvidia')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/customscript')]"
properties:
publisher: Microsoft.HpcCompute
type: NvidiaGpuDriverLinux
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings: {}
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
owner:
type: string
value: "[variables('owner')]"
email:
type: string
value: "[parameters('email')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: '[deployment().name]'
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,410 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageUrn": {
"type": "string",
"defaultValue": "MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest",
"metadata": {
"description": "az vm image list --output table / az vm image list -p RedHat --all --output table, example MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest"
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the local user account"
}
},
"email": {
"type": "string",
"defaultValue": "unused",
"metadata": {
"description": "UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled"
}
},
"projectCode": {
"type": "string",
"defaultValue": "not classified",
"metadata": {
"description": "Uon Project Code"
}
},
"toggleShutdownSchedule": {
"type": "string",
"defaultValue": "f",
"metadata": {
"description": "t / f toggle for ComputeVmShutdownTask"
}
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_B2s",
"metadata": {
"description": "Virtual machine size."
}
},
"networkSecurityGroupName": {
"type": "string",
"defaultValue": "CFWindows",
"metadata": {
"description": "NSG name for CF Linux instances"
}
},
"networkResourceGroup": {
"type": "string",
"defaultValue": "rg-vn-rem-we-1",
"metadata": {
"description": "Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'"
}
},
"virtualNetworkName": {
"type": "string",
"defaultValue": "vn-rem-we-1",
"metadata": {
"description": "Virtual network for CF instances"
}
},
"subnetName": {
"type": "string",
"defaultValue": "sn-vn-rem-we-1-midtier-1",
"metadata": {
"description": "Name of a subnet in the virtual network"
}
},
"location": {
"type": "string",
"defaultValue": "West Europe",
"metadata": {
"description": "Location for all resources, defaults to resource group region"
}
},
"utcValue": {
"type": "string",
"defaultValue": "[utcNow()]"
},
"prefix": {
"type": "string",
"defaultValue": "[uniqueString(resourceGroup().id, parameters('utcValue'))]",
"metadata": {
"description": "passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix"
}
},
"dataDiskType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "storage account type for data disk"
}
},
"dataDiskSizeGB": {
"type": "string",
"defaultValue": "128",
"metadata": {
"description": "data disk size in GB"
}
},
"domainToJoin": {
"type": "string",
"defaultValue": "ad.nottingham.ac.uk",
"metadata": {
"description": "The FQDN of the AD domain"
}
},
"domainUsername": {
"type": "string",
"defaultValue": "service_CloudForms",
"metadata": {
"description": "Username of the account on the domain"
}
},
"domainPassword": {
"type": "securestring",
"defaultValue": "As109pHY4Wi9o7naZnhr#!",
"metadata": {
"description": "Password of the account on the domain"
}
},
"ouPath": {
"type": "string",
"defaultValue": "OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk",
"metadata": {
"description": "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
}
},
"domainJoinOptions": {
"type": "int",
"defaultValue": 3,
"metadata": {
"description": "Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx"
}
}
},
"variables": {
"emailAttributes": "[split(parameters('email'),'@')]",
"owner": "[variables('emailAttributes')[0]]",
"urnAttributes": "[split(parameters('imageUrn'),':')]",
"imagePublisher": "[variables('urnAttributes')[0]]",
"imageOffer": "[variables('urnAttributes')[1]]",
"imageSku": "[variables('urnAttributes')[2]]",
"imageVersion": "[variables('urnAttributes')[3]]",
"resourcePrefix": "[parameters('prefix')]",
"vmName": "[variables('resourcePrefix')]",
"nicName": "[concat(variables('resourcePrefix'), '-nic')]",
"osDiskType": "StandardSSD_LRS",
"networkSecurityGroupId": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
"identityName": "extensionartefact"
},
"resources": [
{
"apiVersion": "2019-11-01",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"OS": "windows",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
}
}
}
],
"networkSecurityGroup": {
"id": "[variables('networkSecurityGroupId')]"
}
}
},
{
"apiVersion": "2019-12-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[parameters('location')]",
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"[resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName'))]": {}
}
},
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"CFSKU": "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]",
"windowsConfiguration": {
"timeZone": "GMT Standard Time"
}
},
"storageProfile": {
"dataDisks": [
{
"diskSizeGB": "[parameters('dataDiskSizeGB')]",
"lun": 0,
"createOption": "Empty",
"managedDisk": {
"storageAccountType": "[parameters('dataDiskType')]"
}
}
],
"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "[variables('osDiskType')]"
}
},
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[variables('imageSku')]",
"version": "[variables('imageVersion')]"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]",
"properties": {
"primary": true
}
}
]
}
}
},
{
"apiVersion": "2015-06-15",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'joindomain')]",
"location": "[parameters('location')]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"properties": {
"publisher": "Microsoft.Compute",
"type": "JsonADDomainExtension",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {
"Name": "[parameters('domainToJoin')]",
"OUPath": "[parameters('ouPath')]",
"User": "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]",
"Restart": true,
"Options": "[parameters('domainJoinOptions')]"
},
"protectedSettings": {
"Password": "[parameters('domainPassword')]"
}
}
},
{
"apiVersion": "2018-06-01",
"condition": "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/', 'customscript')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"ADDomain": "[parameters('domainToJoin')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
],
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": 1.1,
"autoUpgradeMinorVersion": true,
"settings": {
"fileUris": [
"https://extensionartefact.blob.core.windows.net/extensionartefact/Azure_WindowsServer_customscript.ps1"
]
},
"protectedSettings": {
"commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File Azure_WindowsServer_customscript.ps1 -user', ' ', variables('owner'))]",
"managedIdentity": {
"clientid": "[reference(resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName')), '2018-11-30', 'full').properties.clientId]"
}
}
}
},
{
"apiVersion": "2018-09-15",
"condition": "[equals(parameters('toggleShutdownSchedule'), 't')]",
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', variables('resourcePrefix'))]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
],
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": {
"time": 1900
},
"timeZoneId": "GMT Standard Time",
"notificationSettings": {
"status": "Enabled",
"timeInMinutes": 15,
"emailRecipient": "[parameters('email')]"
},
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
}
},
{
"apiVersion": "2015-06-15",
"condition": "[contains(parameters('vmSize'), 'Standard_N')]",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(variables('resourcePrefix'), '/nvidia')]",
"location": "[parameters('location')]",
"tags": {
"CFManaged": true,
"Owner": "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]",
"OwnerMail": "[parameters('email')]",
"Cost Center": "[parameters('projectCode')]"
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]",
"[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/customscript')]"
],
"properties": {
"publisher": "Microsoft.HpcCompute",
"type": "NvidiaGpuDriverWindows",
"typeHandlerVersion": 1.3,
"autoUpgradeMinorVersion": true,
"settings": {}
}
}
],
"outputs": {
"utcOutput": {
"type": "string",
"value": "[parameters('utcValue')]"
},
"adminUsername": {
"type": "string",
"value": "[parameters('adminUsername')]"
},
"adminPassword": {
"type": "string",
"value": "[parameters('adminPassword')]"
},
"vmName": {
"type": "string",
"value": "[variables('vmName')]"
},
"deploymentName": {
"type": "string",
"value": "[deployment().name]"
},
"ipAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"
}
}
}

View File

@ -0,0 +1,315 @@
$schema: https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#
contentVersion: 1.0.0.0
parameters:
imageUrn:
type: string
defaultValue: MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest
metadata:
description: az vm image list --output table / az vm image list -p RedHat --all --output table, example MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest
adminUsername:
type: string
metadata:
description: Local user account for the instance, intended to be used by UoN ops where the instance has no domain connectivity
adminPassword:
type: securestring
metadata:
description: Password for the local user account
email:
type: string
defaultValue: unused
metadata:
description: UoN requester email supplied by cloudforms, where value is 'unused' or 'donotreply@nottingham.ac.uk' the extension rdpgroups is disabled
projectCode:
type: string
defaultValue: not classified
metadata:
description: Uon Project Code
toggleShutdownSchedule:
type: string
defaultValue: f
metadata:
description: t / f toggle for ComputeVmShutdownTask
vmSize:
type: string
defaultValue: Standard_B2s
metadata:
description: Virtual machine size.
networkSecurityGroupName:
type: string
defaultValue: CFWindows
metadata:
description: NSG name for CF Linux instances
networkResourceGroup:
type: string
#defaultValue: unused
defaultValue: rg-vn-rem-we-1
metadata:
description: Populate if the vnet+subnet are in a different resource group (same location) than the instance, otherwise set defaultValue as 'unused'
virtualNetworkName:
type: string
defaultValue: vn-rem-we-1
metadata:
description: Virtual network for CF instances
subnetName:
type: string
defaultValue: sn-vn-rem-we-1-midtier-1
metadata:
description: Name of a subnet in the virtual network
location:
type: string
defaultValue: West Europe
metadata:
description: Location for all resources, defaults to resource group region
utcValue:
type: string
defaultValue: "[utcNow()]"
prefix:
type: string
defaultValue: "[uniqueString(resourceGroup().id, parameters('utcValue'))]"
metadata:
description: passed as param_prefix from cloudforms, the cloudforms stack_name (aka deployment template name) has the same value, default value is to get deterministic hash of resource group and time now for a unique prefix
dataDiskType:
type: string
defaultValue: Standard_LRS
metadata:
description: storage account type for data disk
dataDiskSizeGB:
type: string
defaultValue: "128"
metadata:
description: data disk size in GB
domainToJoin:
type: string
defaultValue: ad.nottingham.ac.uk
metadata:
description: The FQDN of the AD domain
domainUsername:
type: string
defaultValue: service_CloudForms
metadata:
description: Username of the account on the domain
domainPassword:
type: securestring
defaultValue: As109pHY4Wi9o7naZnhr#!
metadata:
description: Password of the account on the domain
ouPath:
type: string
defaultValue: OU=AzureCloudForms_POC,OU=Testing,DC=ad,DC=nottingham,DC=ac,DC=uk
metadata:
description: "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"
domainJoinOptions:
type: int
defaultValue: 3
metadata:
description: Set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx
variables:
emailAttributes: "[split(parameters('email'),'@')]"
owner: "[variables('emailAttributes')[0]]"
urnAttributes: "[split(parameters('imageUrn'),':')]"
imagePublisher: "[variables('urnAttributes')[0]]"
imageOffer: "[variables('urnAttributes')[1]]"
imageSku: "[variables('urnAttributes')[2]]"
imageVersion: "[variables('urnAttributes')[3]]"
resourcePrefix: "[parameters('prefix')]"
vmName: "[variables('resourcePrefix')]"
nicName: "[concat(variables('resourcePrefix'), '-nic')]"
osDiskType: StandardSSD_LRS
networkSecurityGroupId: "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
# subnetRef: "[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in another rg
# subnetRef: "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" # subnet in same rg
identityName: extensionartefact
#rdpgroupsCmd: "[concat('Add-LocalGroupMember -Group ''Remote Desktop Users'' -Member', ' ', variables('owner'))]"
#localadminCmd: "[concat('Add-LocalGroupMember -Group Administrators -Member', ' ', variables('owner'))]"
#disablePopupCmd: "[concat('\"Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask\"')]"
#datadiskCmd: "[concat('\"Get-Disk | Where partitionstyle -eq ''raw'' | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel ''data'' -Confirm:$false\"')]"
##setlocaleCmd: "[concat('Set-WinSystemLocale en-GB;Set-WinUserLanguageList -LanguageList en-GB -Force;Set-Culture -CultureInfo en-GB;Set-WinHomeLocation -GeoId 242;Set-TimeZone -Name \"GMT Standard Time\"')]" # setting the timezone in the osProfile sets these
#installfirefoxCmd: "\"[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));choco install -y firefox googlechrome\""
#powershellCmd: "[concat('powershell.exe -ExecutionPolicy Unrestricted', ' ', variables('rdpgroupsCmd'), ';', variables('localadminCmd'), ';', variables('disablePopupCmd'), ';', variables('datadiskCmd'), ';', variables('installfirefoxCmd'))]"
resources:
- apiVersion: 2019-11-01
type: Microsoft.Network/networkInterfaces
name: "[variables('nicName')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
OS: windows
Cost Center: "[parameters('projectCode')]"
properties:
ipConfigurations:
- name: ipconfig1
properties:
privateIPAllocationMethod: Dynamic
subnet:
#id: "[variables('subnetRef')]" # now use conditional so we can use the subnet in the native RG's vnet or another RG's vnet
id: "[if(equals(parameters('networkResourceGroup'), 'unused'), resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')), resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName')))]"
networkSecurityGroup:
id: "[variables('networkSecurityGroupId')]"
- apiVersion: 2019-12-01
type: Microsoft.Compute/virtualMachines
name: "[variables('vmName')]"
location: "[parameters('location')]"
identity:
type: UserAssigned
userAssignedIdentities:
#"[resourceID('Microsoft.ManagedIdentity/userAssignedIdentities/',variables('identityName'))]": {}
"[resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName'))]": {}
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
CFSKU: "[concat(variables('imagePublisher'), ':', variables('imageOffer'), ':', variables('imageSku'), ':', variables('imageVersion'))]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
properties:
hardwareProfile:
vmSize: "[parameters('vmSize')]"
osProfile:
computerName: "[variables('vmName')]"
adminUsername: "[parameters('adminUsername')]"
adminPassword: "[parameters('adminPassword')]"
windowsConfiguration:
timeZone: GMT Standard Time
storageProfile:
dataDisks:
- diskSizeGB: "[parameters('dataDiskSizeGB')]"
lun: 0
createOption: Empty
managedDisk:
storageAccountType: "[parameters('dataDiskType')]"
osDisk:
createOption: FromImage
managedDisk:
storageAccountType: "[variables('osDiskType')]"
imageReference:
publisher: "[variables('imagePublisher')]"
offer: "[variables('imageOffer')]"
sku: "[variables('imageSku')]"
version: "[variables('imageVersion')]"
networkProfile:
networkInterfaces:
- id: "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
properties:
primary: true
- apiVersion: 2015-06-15
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/', 'joindomain')]"
location: "[parameters('location')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
properties:
publisher: Microsoft.Compute
type: JsonADDomainExtension
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings:
Name: "[parameters('domainToJoin')]"
OUPath: "[parameters('ouPath')]"
User: "[concat(parameters('domainUsername'), '@', parameters('domainToJoin'))]"
Restart: true
Options: "[parameters('domainJoinOptions')]"
protectedSettings:
Password: "[parameters('domainPassword')]"
- apiVersion: 2018-06-01
# conditionally run if owner is a valid domain user and not a placeholder from this template or cloudforms admin user without valid uon email/account
# condition: "[or(not(equals(variables('owner'), 'unused')), not(equals(variables('owner'), 'donotreply')))]" # wont match second condition
condition: "[not(or(equals(variables('owner'), 'unused'), equals(variables('owner'), 'donotreply')))]"
type: Microsoft.Compute/virtualMachines/extensions
#name: "[concat(variables('resourcePrefix'), '/rdpgroups')]"
name: "[concat(variables('resourcePrefix'), '/', 'customscript')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
ADDomain: "[parameters('domainToJoin')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/joindomain')]"
properties:
publisher: Microsoft.Compute
type: CustomScriptExtension
typeHandlerVersion: 1.10
autoUpgradeMinorVersion: true
settings:
fileUris:
- https://extensionartefact.blob.core.windows.net/extensionartefact/Azure_WindowsServer_customscript.ps1
protectedSettings:
#commandToExecute: "powershell.exe Azure_WindowsServer_customscript"
commandToExecute: "[concat('powershell.exe -ExecutionPolicy Unrestricted -File Azure_WindowsServer_customscript.ps1 -user', ' ', variables('owner'))]"
managedIdentity:
clientid: "[reference(resourceId(resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities/', variables('identityName')), '2018-11-30', 'full').properties.clientId]"
- apiVersion: 2018-09-15
condition: "[equals(parameters('toggleShutdownSchedule'), 't')]"
type: Microsoft.DevTestLab/schedules
name: "[concat('shutdown-computevm-', variables('resourcePrefix'))]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
properties:
status: Enabled
taskType: ComputeVmShutdownTask
dailyRecurrence:
time: 1900
timeZoneId: GMT Standard Time
notificationSettings:
status: Enabled
timeInMinutes: 15
emailRecipient: "[parameters('email')]"
targetResourceId: "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
- apiVersion: 2015-06-15
# seems to install driver on MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest if there are issues official support is for 2012 / 2016
# https://docs.microsoft.com/en-us/azure/virtual-machines/windows/n-series-driver-setup
# https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/hpccompute-gpu-windows
condition: "[contains(parameters('vmSize'), 'Standard_N')]"
type: Microsoft.Compute/virtualMachines/extensions
name: "[concat(variables('resourcePrefix'), '/nvidia')]"
location: "[parameters('location')]"
tags:
CFManaged: true
Owner: "[if(equals(parameters('email'), 'unused'), parameters('email'), variables('owner'))]"
OwnerMail: "[parameters('email')]"
Cost Center: "[parameters('projectCode')]"
dependsOn:
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
- "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),'/extensions/customscript')]"
properties:
publisher: Microsoft.HpcCompute
type: NvidiaGpuDriverWindows
typeHandlerVersion: 1.3
autoUpgradeMinorVersion: true
settings: {}
outputs:
utcOutput:
type: string
value: "[parameters('utcValue')]"
adminUsername:
type: string
value: "[parameters('adminUsername')]"
adminPassword:
type: string
value: "[parameters('adminPassword')]"
vmName:
type: string
value: "[variables('vmName')]"
deploymentName:
type: string
value: "[deployment().name]"
ipAddress:
type: string
value: "[reference(resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))).ipConfigurations[0].properties.privateIPAddress]"

View File

@ -0,0 +1,193 @@
#!/bin/bash
exec 1> /root/$0.log 2>&1
set -x
# ubuntu much prefers cloud-init
# should be rewritten with a common function to wait for apt db and metadata availability with timeouts
# Let the system init correctly
sleep 20
# stop ssh to ensure no user login during domain join + update, disable this for debug
systemctl stop sshd
# set tz
timedatectl set-timezone Europe/London
# join domain
# wait loop until apt database is not locked to avoid clash with Azure policy installs, add a little delay to be able to contact apt repos (probably not Azure ones?)
aptupdate=1
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
until [ $aptupdate -eq 0 ];
do
apt-get -y update
apt-get -y install jq
which jq
aptupdate=$?
sleep 5
done
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y install krb5-user samba sssd sssd-tools libnss-sss libpam-sss realmd adcli expect
cat > /etc/krb5.conf <<EOF
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = AD.NOTTINGHAM.AC.UK
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
#default_ccache_name = KEYRING:persistent:%{uid}
[realms]
AD.NOTTINGHAM.AC.UK = {
kdc = AD.NOTTINGHAM.AC.UK
admin_server = AD.NOTTINGHAM.AC.UK
}
[domain_realm]
ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
.ad.nottingham.ac.uk = AD.NOTTINGHAM.AC.UK
EOF
# password cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# if the password changes this script must be reprocessed and the ARM template updated in the CloudForms orchestration template
echo 'As109pHY4Wi9o7naZnhr#!' | kinit -V service_CloudForms@AD.NOTTINGHAM.AC.UK
cat > /etc/realmd.conf <<EOF
[active-directory]
default-client = sssd
[service]
automatic-install = yes
[ad.nottingham.ac.uk]
manage-system = no
automatic-id-mapping = yes
computer-name = $(hostname -s)
computer-ou = ou=AzureCloudForms_POC,ou=Testing,dc=ad,dc=nottingham,dc=ac,dc=uk
EOF
systemctl restart realmd
realm join AD.NOTTINGHAM.AC.UK --install=/
# owner cannot be passed by the ARM template as the whole script is base64 encoded, cloud-init would overcome this limitation
# query metadata tag to find owner, this will be used in the sssd.conf whitelist and sudoers
owner=$(curl -sH Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-06-01" | jq .compute.tags | sed 's/\"//g' | awk -F ";" '{for(i=1;i<=NF;i++)if($i~/Owner:/) split($i,result,":");print result[2] }')
if [ -z "$owner" ]; then
owner=missingtag
fi
cat > /etc/sssd/sssd.conf <<EOF
[sssd]
domains = ad.nottingham.ac.uk
config_file_version = 2
services = nss, pam
[domain/ad.nottingham.ac.uk]
ad_domain = ad.nottingham.ac.uk
krb5_realm = AD.NOTTINGHAM.AC.UK
realmd_tags = joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_sasl_authid = $(hostname -s)$
ldap_id_mapping = True
fallback_homedir = /home/%u@%d
auth_provider = ad
# uon specific with computer account object with no dns
use_fully_qualified_names = False
enumerate = False
# uon large directory performance options
ignore_group_members = True
entry_cache_timeout = 600
# uon search base tuning - target OU for large directory
ldap_user_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
#ldap_group_search_base = OU=Users,OU=University,DC=ad,DC=nottingham,DC=ac,DC=uk
ldap_use_tokengroups = False
# restrict access to owner
access_provider = simple
simple_allow_users = $owner
EOF
# stop sssd until update finished
systemctl stop sssd
systemctl enable sssd
# setup sudoers
cat > /etc/sudoers.d/nottingham <<EOF
$owner ALL=(ALL) NOPASSWD: ALL
EOF
#install desktop, mate desktop meta package requires user interaction to select window manager, expect doesnt play well in whatever shell this script is run from - this minimal install is quicker @10mins
export DEBIAN_FRONTEND=noninteractive
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
apt-get -y --no-install-recommends install x2goserver firefox caja compiz-mate engrampa eom folder-color-caja gnome-accessibility-themes gnome-colors-common gnome-icon-theme gnome-orca grub2-themes-ubuntu-mate indicator-messages indicator-power indicator-session indicator-sound lightdm-gtk-greeter mate-core mate-accessibility-profiles mate-applet-appmenu mate-applet-brisk-menu mate-calc mate-desktop mate-dock-applet mate-hud mate-icon-theme mate-menu mate-menus mate-netbook mate-optimus mate-screensaver mate-screensaver-common mate-system-monitor mate-tweak mate-user-guide mate-utils mate-window-applets-common mate-window-buttons-applet mate-window-menu-applet mate-window-title-applet plank plymouth-theme-ubuntu-mate-logo plymouth-theme-ubuntu-mate-text sessioninstaller sound-theme-freedesktop tilda ubuntu-mate-artwork ubuntu-mate-core ubuntu-mate-default-settings ubuntu-mate-guide ubuntu-mate-icon-themes ubuntu-mate-lightdm-theme ubuntu-mate-themes ubuntu-mate-wallpapers* ubuntu-standard
# enable home directory creation at logon - perform after desktop install to avoid manual prompts with desktop install
sed -i 's/^.*pam_sss.so.*$/&\nsession required\tpam_mkhomedir.so skel=\/etc\/skel\/ umask=0077/' /etc/pam.d/common-session
# add secondary disk
parted -s /dev/disk/azure/scsi1/lun0 -- mklabel gpt mkpart primary xfs 0% 100%
sleep 2 # allow partition to register
mkfs.xfs /dev/disk/azure/scsi1/lun0-part1
sleep 2 # allow filesystem creation to finish
xfs_admin -L uondata /dev/disk/azure/scsi1/lun0-part1
mkdir /uondata
echo "LABEL=uondata /uondata xfs defaults 0 0" >> /etc/fstab
mount -a
chmod 777 /uondata
# install azcopy tool, wait for managed identity to be ready, init azcopy with managed identity token, download UoN installer packages
wget -O azcopy_v10.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy_v10.tar.gz --strip-components=1
mv azcopy /usr/local/bin/
chmod +x /usr/local/bin/azcopy
rm -Rf azcopy*
mkdir /root/uon_packages
identretry=5 # loop with timeout until managed identity is available
msident=1
until [ $msident -eq 0 ] || [ $identretry -eq 0 ];
do
identretry=$((identretry-1))
azcopy login --identity
msident=$?
sleep 5
done
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/CarbonBlackClientSetup-linux-v7.0.0.14291.tgz' '/root/uon_packages'
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/CarbonBlackClientSetup-linux-v7.0.0.14291.sh' '/root/uon_packages'
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/sensorsettings.ini' '/root/uon_packages'
azcopy copy 'https://extensionartefact.blob.core.windows.net/extensionartefact/SophosInstall.sh' '/root/uon_packages'
# install carbon black
chmod +x /root/uon_packages/CarbonBlackClientSetup-linux-v7.0.0.14291.sh
cd /root/uon_packages
#while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done # installer script builds apt packages that can get blocked by the nvidia extension
./CarbonBlackClientSetup-linux-v7.0.0.14291.sh
# install sophos antivirus
chmod +x /root/uon_packages/SophosInstall.sh
./SophosInstall.sh
# clean up
cd /root
rm -Rf /root/uon_packages
# restart services for domain user login
systemctl restart sssd
systemctl restart sshd
#Azure extensions dont like a reboot, dont update without a reboot to ensure no system artifacts
#apt-get -y upgrade
#reboot
#exit gracefully for waagent
exit 0

View File

@ -0,0 +1,21 @@
# ARM template customscript extension command, -user parameter should be the UoN domain user short name, example: ucats OR uizrs
# commandToExecute: "[concat('powershell.exe -ExecutionPolicy Unrestricted -File Azure_WindowsServer_customscript.ps1 -user', ' ', variables('owner'))]"
Param (
[string]$user
)
Add-LocalGroupMember -Group 'Remote Desktop Users' -Member $user
Add-LocalGroupMember -Group Administrators -Member $user
Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask
Get-Disk | Where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel 'data' -Confirm:$false
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco install -y firefox googlechrome azcopy10
New-Item -ItemType Directory c:\uon_packages
azcopy login --identity
azcopy copy https://extensionartefact.blob.core.windows.net/extensionartefact/cbsetup.msi c:\uon_packages\
azcopy copy https://extensionartefact.blob.core.windows.net/extensionartefact/CarbonBlackClientSetup.exe c:\uon_packages\
azcopy copy https://extensionartefact.blob.core.windows.net/extensionartefact/sensorsettings.ini c:\uon_packages\
msiexec /q /i c:\uon_packages\cbsetup.msi /L* c:\uon_packages\log.txt COMPANY_CODE=UON
Start-Sleep -Second 5 # allow msi to finish displaying log before removing the directory
Remove-Item c:\uon_packages -Recurse

View File

@ -0,0 +1,473 @@
#!/bin/bash
# Stores all important files in a tmp directory, then completely uninstalls all
# current versions of cbsensor installed. This will reduce the chance of
# conflicts in the future. When all versions of cbsensor are removed this
# installer will then install the subsystems of the cbsensor and restore the old
# files. If the files are from 6.1.x they will be restored as a zip file,
# otherwise they will be installed in place. Then the sensor will be started.
#
#Globals
###############################################################################
PKG_NAME=CarbonBlackClientSetup-linux-v7.0.0.14291.tgz
PKG_VER=7.0.0.14291
APP_DIR=/var/opt/carbonblack/response
LOGSDIR=$APP_DIR/log
LOG_FILE=$LOGSDIR/install.log
OLDLOGSDIR=/var/log/cb/sensor
PATH=$PATH:/sbin:/usr/sbin:/bin:/usr/bin
RUNNING_KERNEL=$(uname -r)
KERNEL_MAJOR_MINOR=$(echo "$RUNNING_KERNEL" | awk '{split($0,a,"."); print a[1]"."a[2]}')
PLATFORM_INSTALL_DIR=$APP_DIR/pkgs
USE_OLD_META_RPM=0
DATE=$(date)
INSTALLDIR=$(dirname "$SCRIPT")
PKG_PATH=$INSTALLDIR/$PKG_NAME
TMPDIR=$(mktemp -d)
package_manager=""
PKG_MAN=""
CBDAEMON_PKG=""
PKG_EXTENSION=""
OS_ID=""
VER_ID=""
USE_EBPF=false
USE_SYSV=false
USE_SYSD=false
USE_KMOD=false
FLAGS=()
SUBSYSTEMS=(cbsensor cbsysd cbebpf cbsysv cbkmod)
#OS specific stuff
OS_INSTALL=""
OS_REINSTALL=""
OS_DOWNGRADE=""
OS_UPGRADE=""
#Functions
###############################################################################
check_root() {
if [ "$(whoami)" != "root" ]; then
print "Please run the install again as root."
exit 1
fi
}
# Appends '## ' to a string and prints it to the console
print() {
local string="$1"
echo "## $string"
}
# Appends '## ' to a string and writes it to the log file
log() {
local string="$1"
echo "## $string" >>${LOG_FILE}
}
# Appends '## ' to a string and prints it to the console, as well as writes to
# the log file
print_and_log() {
local string="$1"
print "$string"
log "$string"
}
#Use a temp dir to store useful files to be moved after installation
store_saved_files() {
#Store and tar important 6.1.x files
if [[ -d "$OLDLOGSDIR" ]]; then
pushd "$TMPDIR"
"$TAR" czvf cbsensor_logs.old.tgz "$OLDLOGSDIR"
popd
fi
}
restore_saved_files() {
cp "$TMPDIR"/cbsensor_logs.old/ $LOGSDIR
}
set_tools() {
if [ "$(command -v tar)" ]; then
print_and_log "Found tar"
TAR=$(command -v tar)
fi
if [ "$(command -v rpm)" ]; then
package_manager="rpm"
PKG_MAN=$(command -v rpm)
OS_INSTALL="-ivh --replacefiles"
OS_REINSTALL="-ivh --force --replacefiles"
OS_DOWNGRADE="-Uvh --oldpackage --replacefiles"
OS_UPGRADE="-Uvh --replacefiles"
REMOVE_PACKAGE() {
$($PKG_MAN -e "$1")
}
QUERY_INSTALLED_VERSION() {
retval=$($PKG_MAN -qa --queryformat '%{version}' "$1")
retval="${retval//v/}"
# return the version without the 'v'
echo "$retval"
}
QUERY_NEW_VERSION() {
retval=$($PKG_MAN -qp --queryformat '%{version}' "$1")
retval="${retval//v/}"
# return the version without the 'v'
echo "$retval"
}
fi
if [ "$(command -v dpkg)" ]; then
package_manager="dpkg"
PKG_MAN=$(command -v dpkg)
OS_INSTALL="-i --force-overwrite"
OS_REINSTALL="-i --force-overwrite"
OS_DOWNGRADE="-i --force-overwrite"
OS_UPGRADE="-i --force-overwrite"
REMOVE_PACKAGE() {
$($PKG_MAN --purge "$1")
}
QUERY_INSTALLED_VERSION() {
retval=$($PKG_MAN -s "$1" 2> /dev/null | grep -w Version | awk -F": " '{print $2}')
retval="${retval//v/}"
# return the version without the 'v'
echo "$retval"
}
QUERY_NEW_VERSION() {
retval=$(dpkg-deb --info "$1" | grep Version | awk -F ": " '{print $2}')
retval="${retval//v/}"
# return the version without the 'v'
echo "$retval"
}
fi
print_and_log "Using $package_manager as package manager"
}
check_kernel_devel() {
if [ "$USE_EBPF" = true ]; then
if [ "$OS_ID" = "ubuntu" ]; then
kernel_devel=$(dpkg --list | grep linux-headers-generic)
if [ ${kernel_devel} -eq 1 ]; then
print_and_log "The package 'linux-headers-generic' is not installed. Please install kernel-devel."
fi
else
kernel_devel=$(rpm -qa | grep kernel_devel)
if [ ${kernel_devel} -eq 1 ]; then
print_and_log "The package 'kernel_devel' is not installed. Please install kernel-devel."
fi
fi
fi
}
determine_os() {
if [[ -e /etc/os-release ]]; then
OS_ID=$(grep -w "ID" </etc/os-release | awk -F= '{print $2}' | tr -d '"' | tr -d '[:space:]')
elif [[ -e /etc/redhat-release ]]; then
OS_ID=$(cut -d" " -f1 /etc/redhat-release)
elif [[ -e /etc/system-release ]]; then
OS_ID=$(cut -d" " -f1 /etc/system-release)
fi
# Convert OS_ID to all lowercase
OS_ID=${OS_ID,,}
#account for rhel6 "red hat"
if [ "$OS_ID" = "red" ]; then
OS_ID="redhat"
fi
}
set_flags() {
if ls -l /proc/1/exe | grep -w systemd >>/dev/null 2>&1; then
USE_SYSD=true
else
USE_SYSV=true
fi
# If the kernel is 4.4 or later use eBPF.
if version_lt "$KERNEL_MAJOR_MINOR" "4.4"; then
USE_KMOD=true
else
USE_EBPF=true
fi
# If the platform is ubuntu then change the extension
if [ "$OS_ID" = "ubuntu" ]; then
PKG_EXTENSION="deb"
else
PKG_EXTENSION="rpm"
fi
CBDAEMON_PKG="cbsensor-CBDAEMON-$PKG_VER-1.x86_64.$PKG_EXTENSION"
FLAGS+=("$CBDAEMON_PKG")
if [ "$USE_EBPF" = true ]; then
FLAGS+=("cbsensor-EBPF-${PKG_VER}-1.x86_64.$PKG_EXTENSION")
fi
if [ "$USE_SYSV" = true ]; then
FLAGS+=("cbsensor-SYSV-${PKG_VER}-1.x86_64.$PKG_EXTENSION")
fi
if [ "$USE_SYSD" = true ]; then
FLAGS+=("cbsensor-SYSD-${PKG_VER}-1.x86_64.$PKG_EXTENSION")
fi
if [ "$USE_KMOD" = true ]; then
FLAGS+=("cbsensor-KMOD-${PKG_VER}-1.x86_64.$PKG_EXTENSION")
fi
}
version_gt() {
test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"
}
version_lt() {
test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" != "$1"
}
version_ge() {
test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" == "$1";
}
perform_operation() {
print_and_log "############################################################################"
log "$DATE"
print_and_log "Installing cbsensor from $INSTALLDIR"
# Figure out the currently installed version and the new version to install.
oVersion=$(QUERY_INSTALLED_VERSION cbsensor)
nVersion=$(QUERY_NEW_VERSION "$PLATFORM_INSTALL_DIR"/"$CBDAEMON_PKG")
# The default operation is to install
op="--install"
op_txt="install"
# if no old version, then this is a clean install
if [ "$oVersion" = "" ]; then
op=${OS_INSTALL}
op_txt="install"
# If the new version is the same as the installed version we need a reinstall
elif [ "$oVersion" = "$nVersion" ]; then
op=${OS_REINSTALL}
op_txt="reinstall"
# If the installed version is older than the new version we need an upgrade
elif version_lt "$oVersion" "$nVersion"; then
op=${OS_UPGRADE}
op_txt="upgrade"
# If the new version is older than the installed version we need a downgrade
else
version_gt "$oVersion" "$nVersion"
op=${OS_DOWNGRADE}
op_txt="downgrade"
fi
if [[ "$oVersion" == "" ]]; then
print_and_log "$op_txt $nVersion"
else
print_and_log "$op_txt $oVersion -> $nVersion"
fi
for pkg in "${FLAGS[@]}"; do
# UoN change to wait for locked package manager, other agent and extensions will lock dkpg
while fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do sleep 5; done
${PKG_MAN} $op ${PLATFORM_INSTALL_DIR}/"$pkg" | tee -a ${LOG_FILE}
done
}
# servers older than 6.2.2 will deploy the legacy meta rpm instead oF the newer tgz file
# we will need to handle this case here
check_pkg() {
if [[ ! -f ${PKG_PATH} ]]; then
BASE_NAME=$(basename ${PKG_NAME} .tgz)
PKG_NAME=${BASE_NAME}.rpm
USE_OLD_META_RPM=1
PKG_PATH=${INSTALLDIR}/${PKG_NAME}
fi
}
# Checks for multiple sensors installed. If more than one sensor is installed, the newest is kept and the others are removed.
# This only needs to be checked on rpm based systems, dpkg does not allow this to happen.
check_duplicate_sensors() {
print_and_log "Checking for duplicate sensors"
hasDups=$($PKG_MAN -qa | grep cbsensor | awk '{ a=1-a; if(!a) print $0 }')
if [ -n "$hasDups" ]; then
dups=$($PKG_MAN -qa --queryformat '%{version} ' cbsensor 2>/dev/null)
print_and_log "Duplicate sensors detected: $dups"
print_and_log "Attempting to remove $hasDups"
($PKG_MAN -e --nodeps "$hasDups" >>$LOG_FILE 2>&1)
print_and_log "############################################################################"
fi
}
add_daemon() {
print_and_log "Adding daemon..."
if [ "$USE_SYSD" = true ]; then
print_and_log "$(systemctl enable cbdaemon.service 2>&1)"
if [ "$USE_EBPF" = true ]; then
print_and_log "$(systemctl enable cbebpfdaemon.service 2>&1)"
fi
else
print_and_log "$(chkconfig --add cbdaemon 2>&1)"
fi
if [ "$USE_EBPF" = false ]; then
print_and_log "$(sh /etc/sysconfig/modules/cbresponse.modules 2>&1)"
fi
print_and_log "$(chmod 600 /var/opt/carbonblack/response 2>&1)"
print_and_log "Starting daemon..."
if [ "$USE_SYSD" = true ]; then
print_and_log "$(systemctl restart cbdaemon.service 2>&1)"
if [ "$USE_EBPF" = true ]; then
print_and_log "$(systemctl restart cbebpfdaemon.service 2>&1)"
fi
else
#Fix for reinstalling the package on a system with sysV
if pgrep cbdaemon >/dev/null 2>&1; then
print_and_log "$(/etc/init.d/cbdaemon restart 2>&1)"
else
print_and_log "$(/etc/init.d/cbdaemon start 2>&1)"
fi
fi
print_and_log "CB Response Sensor is now installed"
}
check_necessary_files() {
if [[ ! -f "$PKG_PATH" ]]; then
print_and_log "Error: Installation Package is not present"
exit 2
fi
if [[ ! (-f "$INSTALLDIR"/sensorsettings.ini) ]]; then
print_and_log "Error: sensorsettings.ini file is not present"
exit 2
else
cp "$INSTALLDIR"/sensorsettings.ini "$APP_DIR"
chmod 644 "$APP_DIR"/sensorsettings.ini
fi
}
check_meta_rpm() {
# Make sure the older meta package is no longer present from previous installations
set +e # If the RPM isn't installed, we expect to catch an error in rval. Don't abort on error.
CB_RPM_UNIST=("cb-linux-sensor" "cb_package-UNIFIED_RPM")
for val in "${CB_RPM_UNIST[@]}"; do
${RPM} -q "$val" >/dev/null 2>/dev/null
rval=$?
if [[ ${rval} -eq 0 ]]; then
echo "## Remove old meta rpm: "$val"" | tee -a ${LOG_FILE}
${RPM} -e "$val" >>${LOG_FILE} 2>&1
echo "############################################################################" | tee -a ${LOG_FILE}
fi
done
set -e # Re-enable abort on error.
}
# Install using tarball or legacy meta RPM
extract_package() {
print_and_log "Extracting package"
if [[ ${USE_OLD_META_RPM} == 0 ]]; then
rm -f "${INSTALLDIR}"/*.rpm # Remove any old RPMs first
(${TAR} xvzf "${PKG_PATH}" -C ${PLATFORM_INSTALL_DIR} | tee -a ${LOG_FILE})
else
(${RPM} -i "${PKG_NAME}" >>${LOG_FILE})
fi
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
print_and_log "install failed on package extraction"
exit 1
fi
}
# Exit on error
set -e
build_directory_structure() {
mkdir -p "$LOGSDIR"
touch $LOG_FILE
print_and_log "Building directory structure"
mkdir -p "$APP_DIR"/eventlogs/finalized
mkdir -p "$APP_DIR"/pkgs
mkdir -p "$APP_DIR"/store
}
# Make sure the older meta package is no longer present from previous installations
remove_old_meta_package() {
CB_RPM_UNIST=("cb-linux-sensor" "cb_package-UNIFIED_RPM")
for val in "${CB_RPM_UNIST[@]}"; do
rpm -q "$val" >/dev/null 2>/dev/null
rval=$?
if [[ ${rval} -eq 0 ]]; then
print_and_log "Removing old meta rpm"
(${RPM} -e "$CB_RPM_UNIST" >>${LOG_FILE} 2>&1)
print_and_log "############################################################################"
fi
done
}
cleanup() {
print_and_log "Cleanup setup packages"
for file in "${PLATFORM_INSTALL_DIR}"/*; do
if [[ -e "$file" ]]; then
rm -f ${PLATFORM_INSTALL_DIR}/*
fi
done
#
# Delete the old logs directory
#
if [[ -d ${OLDLOGSDIR} ]]; then
rm -rf ${OLDLOGSDIR}
fi
}
# 6.1.x and earlier versions of the sensor stored data in /var/lib/cb
# Since we are upgrading to a new file structure, delete the old files structure
# Leaving it in place causes problems with future upgrades.
# We make no claims regarding the ability to downgrade from 6.2.x to 6.1.x or earlier.
rm -rf /var/lib/cb
main() {
check_root
build_directory_structure
check_necessary_files
check_pkg
set_tools
determine_os
set_flags
extract_package
#Temp fix until ubuntu stuff is added
if [ "$package_manager" = "rpm" ]; then
check_duplicate_sensors
fi
perform_operation
add_daemon
}
main
trap cleanup EXIT

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,29 @@
[CB]
ConfigName=LIVE
CollectStoreFiles=1
CollectModuleLoads=1
CollectModuleInfo=1
CollectFileMods=1
CollectRegMods=1
CollectNetConns=1
CollectProcesses=1
CollectCrossProcess=1
CollectEmetEvents=1
CollectProcessUserContext=1
CollectDataFileWrites=1
SensorBackendServer=https%3A//sensors.fit-turtle.my.cbcloud.de%3A443
SensorClientCert=-----BEGIN%20CERTIFICATE-----%0AMIICvjCCAaagAwIBAgIQBDVK9y8pT2uelTCFmUI5KjANBgkqhkiG9w0BAQsFADAR%0AMQ8wDQYDVQQDDAZzZXJ2ZXIwHhcNMTkwNDIzMTAzMTU3WhcNMjkwNDIwMTAzMTU3%0AWjAlMSMwIQYDVQQDDBpTZW5zb3IgR3JvdXBbMl06ICdTdGFnaW5nJzCCASIwDQYJ%0AKoZIhvcNAQEBBQADggEPADCCAQoCggEBALphRJO9g%2Bcm8Ezv7SZE9L98xImGEPj%2B%0A4uPMF2kvHl/VG/NjvZN7ihrRELGlTBCwlEaTIMuJflQiXhCkU9qW9T89dO%2BLcoaE%0AxNt23drEjhvJufOn4JrXt72foWPrkiu80x2HzI/5AxfNgohvcu%2By1posEH/5ggiy%0ALgksLTi9jZR9div7BaecbIrI8OGnYNoSrc4vicxpRWTIDOZ3qmOqZVD/%2B0TcQ6DT%0Am0dXD9SETqmghOg7J0mgLZfWUM1bSdQnQVi5AeeAtASF824n71DeHYBTH1oR5Tib%0AREIijBsXd7f7R8YjfWs%2BYbWN5ajTcC5xmYwzhthHeQOJb4Irjk5e4pUCAwEAATAN%0ABgkqhkiG9w0BAQsFAAOCAQEAdtJMzYbjHzLwRRn8gwtD3PAc3Lc9ZyrVEDLoAwXo%0A/ZpSmN9%2BpFz6L084TSn3AVs1zThmSdFEJ9z4hviggPR1P8/XQIH3XJwioK79vg/x%0AQ3SUNbwbJeDaAQTkWcWQXQmH/LNdwB/G3sKkaOydN3QDScuncCsVdjnC2WDGNF8I%0A4zbdsZVHwx6%2Brkr129yTZIaB7xRmE/SP6sY0enfVIG8t20t1QMjhTa6hUG/jEWsv%0ANUYO5bNzXND5GcT8t8goBb8OYEMN0e8peg3UKXP3snzbeMh3WxQywV3J7xusC2vC%0AlujdVS5hchWRiwwNB7QN8nzilbTPDbb9Fm/ffZ9yDrZQAQ%3D%3D%0A-----END%20CERTIFICATE-----%0A
SensorClientKey=-----BEGIN%20PRIVATE%20KEY-----%0AMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6YUSTvYPnJvBM%0A7%2B0mRPS/fMSJhhD4/uLjzBdpLx5f1RvzY72Te4oa0RCxpUwQsJRGkyDLiX5UIl4Q%0ApFPalvU/PXTvi3KGhMTbdt3axI4bybnzp%2BCa17e9n6Fj65IrvNMdh8yP%2BQMXzYKI%0Ab3LvstaaLBB/%2BYIIsi4JLC04vY2UfXYr%2BwWnnGyKyPDhp2DaEq3OL4nMaUVkyAzm%0Ad6pjqmVQ//tE3EOg05tHVw/UhE6poIToOydJoC2X1lDNW0nUJ0FYuQHngLQEhfNu%0AJ%2B9Q3h2AUx9aEeU4m0RCIowbF3e3%2B0fGI31rPmG1jeWo03AucZmMM4bYR3kDiW%2BC%0AK45OXuKVAgMBAAECggEAawbkHHrdveVsZKH6atl6OmPpcJeeM34ayHkwkGRQavOz%0Aw0ZpXMG6gr%2B/eGPVlFfyLbzbPkZMVwANSD01MfcyCgi%2Bl7haIckoSmat66yndmmW%0A8MZcgk6R4sBCK2DhZWBfUzviSmLSVd7bsIFfXSozdgEL0JF1DI1VRksqBMVFAhk1%0ArEbnfIm9MD5HHhAl3WaAVqlTaFYU0SK7%2BrM7Eb4UtQyBGLpnfNLdB6L7C0a8MJSp%0Ao1cIT92HROozysGTZUrUinALsuEObz43FSjIjXknyBZKoPP5Cq8Z4Hy9nZEV9eRA%0ATKAfIt5L82F0xu1Mu0Wfb8TEHTOvUd0WW/93Ot31GQKBgQDw0H838gNQP/U6GRav%0AlQTZkE68j4bNs/fNmDp4tTG3CAOmjHslInzUwf9XwgRyZcJCkJsj1tTpfJUsSPA/%0AFuOmSQXIwMy/4q2eo1hCJPauaom7tGxOxF7elrl/8swfnyXRD6uNH%2BUlZdaAu2b4%0A7wxRSQzPYrzdy4oqkQC85sSWIwKBgQDGIgbmWe9MW55tvMZgjlcUIppbIyleFAyh%0AjJTL4mgKBxN/sGyLrTfrxGaELp81lbHdjvpkWUXLyIcbXk1Qigpwl6XnPpZzHdq8%0Arp%2BIA%2Bh8jxX6nDN4qzmopf3QfCmihZMuI6S9mK6BIFJS3xH4M/AkDPnSZY6oL%2BZK%0AHjou1vMD5wKBgA5ys%2B9LWd%2BKts/RiYbnTe8vtUmi4tecoJV5OKjdVipBFNb9PrYv%0Ap4WsTgGZ5mJmsI0F2AkCbqvDib%2BqcJ%2BYY/gqEWrGBoLMutX1vunZBePZCIJ2hLkF%0AFxwLtv1yN8T962rrocNJ0pm3znLSy47L6NVHqLkYl3RHLfb31C%2BBOPI5AoGAMNlm%0A1hhKVYspLmkih9/QPFijseCjbFyJGLNuZC439HD2L78xo%2BZbKTfNBr3v5ug3aCa%2B%0AOUuBV9Li7K59ZWQDWusorjDSKyOrMGXlU0WTZlJo2tb0IcYlex0hzOsv4LAKL4/%2B%0AJ/ii3Zc4dNImvgkgJeNFHMiJOZJFtvRo5%2B97DvUCgYB7gbPFXAnUfSfad3hmnHWS%0AYmX609o47kwsQ4YjOGENYyU/NPVhbqWKRxHkVkaSIIvbCLcgZ9M8UPgiYUf2kvt6%0APqcQJ72ktgf5HzBxTveCc45xn15Cffu9N7EV4FnwcRqWtUcpXQVJYk1fObKL2eL5%0AG9v3zLBL6eKb9CAul8tapA%3D%3D%0A-----END%20PRIVATE%20KEY-----%0A
CbServerCert=-----BEGIN%20CERTIFICATE-----%0AMIICnjCCAYagAwIBAgIEXKFjwTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZz%0AZXJ2ZXIwHhcNMTkwNDAxMDEwNTA1WhcNMjkwMzI5MDEwNTA1WjARMQ8wDQYDVQQD%0ADAZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu1ygbPU5f%0AMPFSBugTrZnguPpwizJvUyTrIteuzN4NWKhIH05VeG49ghypFiEDOk31IfN5%2Bj1s%0AP8bYsj5U0TYrE4nG%2BMJwCFEroj%2B/KrIk5kuak69m15MrRagdYdJe5ev4wcIGcyDV%0A9dvXN0qBoWxL%2BK9oBrdXKBrHWx8PkL9Q8Zd8EvJ90QhXG4l1UiGAVM968gUUV%2Bmx%0AeMfONhKgW3cQD0R6QcR7PwjRMOLNeNDvbcSnpP%2BEhY3BGAUGjzgriLGrYotW0/Aq%0Ak%2B4yXeMtd1fxq0C8zaFgqrW/NKahgmifzLSZd034j74oCa2U9jffthWe2UFgUhcd%0AJF0xl4ilzQzjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEgll3nCYKIoGhzIJbgO%0A4Y9yOUOTsQ/fKgbj5%2BKXy%2BLMnues3feF3uLQ0vaEl4BE/%2BJbI0cGo27fTNz/I7Wk%0ABp6WSHyBeV93WwRTNyeIjW0ytVual9i5d04/M38ksvKHmzep9P3i%2BJ%2BWYSAanDCG%0A0gHTyS3NKrYSONOF5vCn5M0LucCWVd77ziOChcHi1/kO1%2BHm4hbRv7PldVeS8C8/%0ArQvuq7DFPESqtMOX%2BTMLT4CcdiI%2BjQ0HZhoU4YirEp53WGFAixuq0e3ARS/VGK4K%0AF14A7DlzTYm84u8Ru7J/cgtrHUHUB5%2BrzzYfV1KyjT8hLwokB6To9y7jni%2BLBdq4%0Aj60%3D%0A-----END%20CERTIFICATE-----%0A
MaxLicenses=-1
SensorVersion=Manual
QuotaEventlogPercent=1
QuotaEventlogBytes=536870912
QuotaStorefileBytes=536870912
QuotaStorefilePercent=1
VdiEnabled=0
TamperLevel=0
SensorExeName=
ProcessFilterLevel=1
FilterKnownDLLs=1
CbServerSSLCertStrictCheck=0

48
README.md Executable file
View File

@ -0,0 +1,48 @@
# What is this?
A collection of parametrized (linux/windows/GPU) ARM templates used to self service provision various flavours of research VMs.
The ARM templates slot into a Azure cloud infrastructure designed like an on-premesis network (not sure why), as such the templates join the VMs to a classic Windows Active Directory apply strict policies and install corporate spyware fromm private blobs, a sort of limitless hypervisor.
A collection of parameterized Ansible playbooks designed to run as scripts for:
allow end users to add themselves to various AD groups for service access (ansible running winrm and remote powershell)
allow end users to provision SAMBA shares from various storage platforms, either corporate or HPC (ansible URI API calls)
## This repo contains the assets used in the cloudforms self service project
| Template | Description |
|--|--|
| ansible-ad-group-add | Adds AD users to AD groups, with self service logic and email notification, parameterised for reuse with different groups and email templates. |
| ansible-gpfs-samba-provision | Creates GPFS samba shares, ready to be parameterised for cloudforms, not in production, emails not pretty for customers. |
| ansible-netapp-qtree-provison | Creates qtrees with AD ACL's on a target volume shared via samba, ready to be parameterised for cloudforms, not in production, no emails. |
| ARM_templates/prod_1.0 | ARM templates to deploy Azure instances and associated networking in different prod RG's, include scripts and extensions that join UoN AD. |
| ARM_templates/prod_1.1 | ARM templates to deploy Azure instances and associated networking in different prod RG's, include storage container access to pull installer scripts/packages, broadly joining domain, installing desktop components, formatting disk, setting permissions and installing carbon black. |
| ARM_templates/dev | ARM templates to deploy Azure instances in dev RG |
Please refer to the README.md in each folder for usage and further details.
## Included playbooks and ARM templates
├── ansible-ad-group-add
│   ├── templates
│   │   ├── default
│   │   └── transcription
│   └── vars
├── ansible-gpfs-samba-provision
│   ├── tasks
│   ├── templates
│   └── vars
├── ansible-netapp-cifs-provision
│   └── vars
├── ansible-netapp-qtree-provison
│   ├── templates
│   └── vars
└── ARM_templates
├── dev
│   ├── rev1
│   ├── rev2
│   └── rev3
├── prod_1.0
└── prod_1.1
└── extensionartefacts

191
ansible-ad-group-add/README.md Executable file
View File

@ -0,0 +1,191 @@
# Usage
Should be run from a Cloudforms tile with parameters passed on runtime, there should be no need to edit the variables main.yml.
Cloudforms tiles pass the following mandatory parameters:
groupmembers="user" / "user1,user2,userN"
group=ad-group
perform=create / delete
ad_host=fqdn-of-ad-server
ad_user="user" / "user@DOMAIN.COM"
ad_pass="account-password"
from_email=donotreply@nottingham.ac.uk
api_user=cloudforms-user-with-api-write
api_pass=cloudforms-user-pass
- ad_pass / api_pass should have value in quotes to allow any special characters, all parameter accept quoted
- ad_user must have sufficient rights to administer group and query user object properties
- email_requester passed by cloudforms using API
- from_email address should be allowed through relay whitelist
More parameters can be passed, check vars/main.yml for all functional parameters, these include an output toggle, winrm connectivity and email toggles.
It is not recommended to hard code sensitive parameters such as passwords in the playbook vars/main.yml, these should be stored in Cloudforms which encrypts these in the database.
## Failure conditions
Any items in vars/main.yml that have the value placeholder that are not passed as parameters on runtime will fail the playbook.
Invalid users that are not in ActiveDirectory.
Connectivity issues with ActiveDirectory or SMTP relay. Ansible module error output will be observed in these scenarios.
## Note
Parameters can be passed in any order.
The groupmembers field will validate only populated comma delimited entries, the following will be accepted:
",,,tseed,,swright,,"
ActiveDirectory user objects have associated email addresses pulled from their published properties. Job updates will be sent to these email accounts.
Failure emails where ActiveDirectory users are invalid are only sent to the requester.
## Example command to run playbook on the command line
ansible-playbook adgroup.yml -e 'groupmembers="tseed,swright" \
group=Project \
perform=delete \
ad_host=WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL \
ad_user="administrator@NETAPPSIM.LOCAL" \
ad_pass="Password0" \
from_email=tseed@ocf.co.uk \
enable_requester_email=false \
enable_customer_email=false \
api_user=dummy \
api_pass=dummy'
## Example command to run playbook on the command line and send emails with custom customer email templates
ansible-playbook adgroup.yml -e 'groupmembers="tseed,swright" \
group=Project \
perform=delete \
ad_host=WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL \
ad_user=administrator \
ad_pass="Password0" \
from_email="noreply@cloudforms" \
enable_requester_email=true \
enable_customer_email=true \
smtp_relay=192.168.101.240 \
smtp_port=25 \
template_prefix=transcription \
requester_email=tseed@ocf.co.uk \
api_user=dummy \
api_pass=dummy '
## Self service mode
The initial design of the script catered for user(s) being added to a group and the requester getting status emails for add/remove/invalid-user/no-change operations and the users to receive add/remove emails.
An updated use case where the requester user populates the groupmembers parameter only with its own username is known as the self service model.
This model effectively disables the requester status emails and will send add/remove/no-change user emails to the requester.
To replicate this behaviour on the command line ensure the following parameter is passed with the above example syntax, and ensure the groupmember parameter only has a single user account entry.
groupmembers="tseed"
spoof_self_service=true
## Email behaviour
When template_prefix is omitted the value is set to default and default email templates used, in this scenario customer emails are not sent disregarding the parameter enable_customer_email.
To add service specific emails, create a new directory under the templates directory, populate email templates and pass matching parameter template_prefix=<my_new_service>.
Requester email templates are prefixed mail-, customer email templates customer-, there are 4 conditions in which templates are suffixed - add / remove / invalid / nochange.
Name the email templates in accordance with this convention, e.g mail-add.j2.
Customers will receive an add / remove / no-change email only when an action has been performed upon their account, customer emails are in html format.
Cloudforms local accounts such as admin do not have an email address, to debug with said accounts add parameter requester_email=<your uon email>

550
ansible-ad-group-add/adgroup.yml Executable file
View File

@ -0,0 +1,550 @@
---
- name: Query automate workspace
hosts: localhost
gather_facts: False
vars_files:
vars/main.yml
tasks:
- set_fact:
endpoint: "{{ manageiq.api_url }}"
auth_token: "{{ manageiq.api_token }}"
request_id: "{{ (manageiq.request).split('/')[-1] }}"
service_id: "{{ (manageiq.service).split('/')[-1] }}"
user_id: "{{ (manageiq.user).split('/')[-1] }}"
when: manageiq is defined
# when users run ansible their manageiq auth token does not have sufficient rights to interact with the API, no combination of rights in a role for a non admin user are sufficient
# use the local admin credentials to get an auth token
- name: Get auth token
uri:
# ansible runner timeout, something changed from 5.11.1.2 to 5.11.6.0?
#url: "{{ endpoint }}/api/auth"
url: "https://127.0.0.1/api/auth"
validate_certs: no
method: GET
user: "{{ api_user }}"
password: "{{ api_pass }}"
status_code: 200
register: login
when: manageiq is defined
- set_fact:
auth_token: "{{ login.json.auth_token }}"
when: manageiq is defined
- name: Get requester user attributes
uri:
#url: "{{ endpoint }}/api/users/{{ user_id }}"
url: "https://127.0.0.1/api/users/{{ user_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: user
when: manageiq is defined
- set_fact:
requester_email: "{{ user.json.email }}"
when: manageiq is defined and user.json.email is not none # cloudforms admin user has no email address (unless set), this is a null field in json
- set_fact:
requester_email: "{{ from_email }}" # from_email should be able to recieve mail via relay
when: requester_email is not defined
- set_fact:
requester_user: "{{ user.json.name }}"
when: manageiq is defined
# when run from the command line set a default requester user
- set_fact:
requester_user: "command line invocation"
when: manageiq is not defined
# - name: DEBUG print all user attributes
# debug:
# msg:
# - "{{ user }}"
#
# useful fields:
# "email": "ucats@exmail.nottingham.ac.uk"
# "name": "Toby Seed"
# "userid": "toby.seed@nottingham.ac.uk"
#
# we see that the UON active directory schema has a different correlation between the AD short login id and the lookup of the userid and name
# the above user generally would use the login id "ucats" across the UON estate to authenticate against AD
# interestingly cloudforms queries several fields in the schema and will allow login as "ucats" and "toby.seed@nottingham.ac.uk"
# to detect if this script is being run in self service mode, a match is performed against the requesting user and a single entry in groupmember list
# we can only retrieve the expected AD short login id from the email field using UON AD
# remove after testing - self service mode wasnt tested enough on dev system where cloudforms admin has no email
# - name: get AD account name
# set_fact:
# requester_user_ad: "{{ (user.json.email).split('@')[0] }}"
# when: manageiq is defined
- name: get AD account name
set_fact:
requester_user_ad: "{{ (requester_email).split('@')[0] }}"
when: manageiq is defined
- name: get service
uri:
#url: "{{ endpoint }}/api/services/{{ service_id }}"
url: "https://127.0.0.1/api/services/{{ service_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: service
when: manageiq is defined
- set_fact:
service_name: "{{ service.json.name }}"
new_service_name: "{{ service.json.name }} {{ request_id }}"
when: manageiq is defined
- set_fact:
service_name: "command line invocation"
new_service_name: "command line invocation"
when: manageiq is not defined
- name: set service name
uri:
#url: "{{ endpoint }}/api/services/{{ service_id }}"
url: "https://127.0.0.1/api/services/{{ service_id }}"
validate_certs: no
method: POST
headers:
X-Auth-Token: "{{ auth_token }}"
body_format: json
body: { "action" : "edit", "resource" : { "name" : "{{ new_service_name }}" }}
status_code: 200, 204
register: service
when: manageiq is defined
- hosts: localhost
gather_facts: false
name: Validation tasks
vars:
groupmemberslist: []
groupmemberslistvalidate: []
vars_files:
vars/main.yml
tasks:
- name: Build inventory for AD server
add_host: >
name=adserver
groups=windows
ansible_host="{{ ad_host }}"
- name: Fail Where Requisite Vars Not Set
fail:
msg: "Parameter {{item.key}} has value {{item.value}}, {{item.key}} is required to be passed from Cloudforms"
when: item.value == 'placeholder'
loop: "{{ lookup('dict', vars ) }}" #vars is a special variable of a list containing all variables in the playbook
no_log: "{{ suppress_vars_output }}"
- name: Split groupmembers parameter on , delimiter
set_fact:
groupmemberslist: "{{ groupmemberslist }} + [ '{{ item }}' ]"
with_items: "{{ groupmembers.split(',') }}"
- name: Remove empty fields from groupmembers parameter
set_fact:
groupmemberslistvalidate: "{{ groupmemberslistvalidate }} + [ '{{ item }}' ]"
when: item | length != 0
with_items: "{{ groupmemberslist }}"
- hosts: adserver
gather_facts: false
name: Check AD user exists
vars:
# set present/absent flag for netapp modules from create/delete values in the perform parameter, included here for action to perform
state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}"
groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}"
email_customer: []
no_aduser: []
email_recipients: []
user_no_action: []
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
# NOT USED body_requester_user_ad: "{{ hostvars['localhost']['requester_user_ad }}"
vars_files:
vars/main.yml
tasks:
# to avoid using group_vars we set_facts that were not accepted by add_host, add_host does not work with many windows winrm connectivity vars
- name: Add connectivity variables for adserver
set_fact:
ansible_user: "{{ ad_user }}"
ansible_password: "{{ ad_pass }}"
ansible_connection: "{{ ad_connection }}"
ansible_winrm_transport: "{{ ad_winrm_transport }}"
ansible_winrm_kinit_mode: "{{ ad_winrm_kinit_mode }}"
ansible_winrm_message_encryption: "{{ ad_winrm_message_encryption }}"
ansible_port: "{{ ad_port }}"
ansible_winrm_scheme: "{{ ad_winrm_scheme }}"
ansible_winrm_server_cert_validation: "{{ ad_winrm_server_cert_validation }}"
- name: Check AD user exists
win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne()
register: command_result
with_items: "{{ groupmemberslistvalidate }}"
- name: Flag fail where AD user not exist
set_fact:
no_aduser: "{{ no_aduser }} + [ '{{ item.item }}' ]"
when: item.stdout | length == 0
with_items: '{{ command_result.results }}'
changed_when: true
notify: topic_noad
- name: Get Domain user email address
win_shell: Get-ADUser {{ item }} -Properties mail | Select-Object -ExpandProperty mail
register: email_result
with_items: "{{ groupmemberslistvalidate }}"
when: no_aduser | length == 0
# this will crash out where customer account has no associated email - needs logic for empty elements
- name: Add customer email to list of email recipients
set_fact:
email_customer: "{{ email_customer }} + [ '{{ item.stdout_lines[0] }}' ]"
with_items: "{{ email_result.results }}"
when: no_aduser | length == 0
- name: Build list of user and accociated email address
set_fact:
account_email: "{{ account_email | default([]) + [dict(name=item[0], email=item[1])] }}"
loop: "{{ groupmemberslistvalidate | zip (email_customer) | list }}"
when: no_aduser | length == 0
- name: Add Domain user/group to Domain Group
win_domain_group_membership:
name: "{{ group }}"
members: "{{ groupmemberslistvalidate }}"
state: "{{ state }}"
register: result
when: no_aduser | length == 0
- name: Build list of user emails requiring notification for add operation
set_fact:
email_recipients: "{{ email_recipients | default([]) + [item.email] }}"
with_items: "{{ account_email }}"
when: ( no_aduser | length == 0 and (result.added | length > 0 and state == 'present')) and item.name in result.added
- name: Build list of user emails requiring notification for delete operation
set_fact:
email_recipients: "{{ email_recipients | default([]) + [item.email] }}"
with_items: "{{ account_email }}"
when: ( no_aduser | length == 0 and (result.removed | length > 0 and state == 'absent')) and item.name in result.removed
# users in this list were not added/removed from AD group as they were already present/not-present
- name: Build a list of users where no action was taken
set_fact:
user_no_action: "{{ user_no_action | default([]) + [item.name] }}"
with_items: "{{ account_email }}"
when: no_aduser | length == 0 and (item.name not in result.removed and item.name not in result.added)
- name: Build a list of emails for users where no action was taken
set_fact:
email_recipients_no_action: "{{ email_recipients_no_action | default([]) + [item.email] }}"
with_items: "{{ account_email }}"
when: no_aduser | length == 0 and (item.name not in result.removed and item.name not in result.added)
# - debug:
# msg:
# - "email recipients change {{ email_recipients }}"
# - "email recipients nochange {{ email_recipients_no_action }}"
# - "users not actioned {{ user_no_action }}"
- set_fact:
template_prefix: "default"
when: template_prefix is not defined
handlers:
- name: Build invalid user(s) email body
set_fact:
mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-invalid.j2') }}"
delegate_to: localhost
listen: topic_noad
- name: Send status email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ hostvars['localhost']['requester_email'] }}"
subject: "Result of request {{ body_service_name }} : {{ perform }} user(s) in {{ group }}"
body: "{{ mail_body }}"
delegate_to: localhost
when: hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true'
listen:
- topic_noad
- name: Condition fail on invalid AD user
debug:
msg:
- "invalid AD user(s) to {{ perform }} in group {{ group }}"
- ---------------------------------------
- "{{ no_aduser }}"
- ---------------------------------------
delegate_to: localhost
listen: topic_noad
# provides failure exit code for cloudforms and terminates playbook
- name: Hard exit
fail:
listen: topic_noad
- hosts: localhost
gather_facts: false
name: Report Results
vars:
state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}"
result: "{{ hostvars['adserver']['result'] }}"
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
body_service_name_noreqid: "{{ hostvars['localhost']['service_name'] }}" # service_name matches cloudforms tile name (i.e Automated Transcription Service), we want this for the customer email subject line - welcome to <service_name>
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
body_requester_user_ad: "{{ hostvars['localhost']['requester_user_ad'] }}"
groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}"
email_recipients: []
email_recipients_no_action: []
user_no_action: []
self_service_user: false
vars_files:
vars/main.yml
tasks:
# the initial design of the script catered for user(s) being added to a group and the requester getting status emails (add / remove / invalid-user / no-change) and the user(s) recieving (add / remove) emails
# a use case where the requester is requesting only himself could mean status emails and user emails being recieved
# logic at the end of this script evaluates if the requester user matches a single entry in groupmember list and then disables requester/status emails
# to test this behaviour on the command line ensure the following fact (requester_user_ad) is set to the same value as the single item in the groupmembers parameter
# this condition is replicated with the spoof_self_service parameter
- name: checking for spoof self service mode
set_fact:
body_requester_user_ad: "{{ groupmemberslistvalidate[0] }}"
when: spoof_self_service == "true"
- name: detect if the requester user is adding/removing only itself to group (only active when using a custom template_prefix)
set_fact:
self_service_user: true
when: (groupmemberslistvalidate | length == 1 and groupmemberslistvalidate[0] == body_requester_user_ad) and template_prefix != 'default'
- debug:
msg: "running in self service mode"
when: self_service_user
- name: set list of email recipients where action has been taken upon their account
set_fact:
email_recipients: "{{ hostvars['adserver']['email_recipients'] }}"
when: hostvars['adserver']['email_recipients'] is defined
- name: set list of email recipients where no action has been taken upon their account
set_fact:
email_recipients_no_action: "{{ hostvars['adserver']['email_recipients_no_action'] }}"
when: hostvars['adserver']['email_recipients_no_action'] is defined
- name: set list of user names where no action has been taken upon their account
set_fact:
user_no_action: "{{ hostvars['adserver']['user_no_action'] }}"
when: hostvars['adserver']['user_no_action'] is defined
- name: Print email recipient information
debug:
msg:
- "requester to recieve status email: {{ requester_email }}"
- "enable requester mail: {{ enable_requester_email }}"
- "enable customer mail: {{ enable_customer_email }}"
- "self service mode: {{ self_service_user }}"
- "user(s) that can recieve change notification: {{ email_recipients }}"
- "user(s) with no action undertaken: {{ user_no_action }}"
- "user(s) that can recieve nochange notification: {{ email_recipients_no_action }}"
- set_fact:
template_prefix: "default"
when: template_prefix is not defined
- name: Build nochange email status body
set_fact:
mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-nochange.j2') }}"
when: not result.changed
- debug: # changed_when: false in 'build nochange email' doesnt force notification of topic, use debug as a work around to always get a changed: true status to ensure a desired handler is executed
msg: "Invoke nochange handler"
changed_when: true
when: not result.changed
notify: topic_nochange
- name: Build add email status body
set_fact:
mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-add.j2') }}"
changed_when: true
when: result.changed and result.added |length > 0
notify: topic_add
- name: Build remove email status body
set_fact:
mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-remove.j2') }}"
changed_when: true
when: result.changed and result.removed |length > 0
notify: topic_remove
- name: Build add email customer body
set_fact:
customer_change_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-add.j2') }}"
changed_when: true
when: (result.changed and result.added |length > 0) and template_prefix != 'default'
notify: topic_add
- name: Build remove email customer body
set_fact:
customer_change_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-remove.j2') }}"
changed_when: true
when: (result.changed and result.removed |length > 0) and template_prefix != 'default'
notify: topic_remove
- name: Build nochange add email customer body
set_fact:
customer_nochange_add_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-nochange.j2') }}"
changed_when: true
when: user_no_action |length >0 and template_prefix != 'default'
notify: topic_nochange
handlers:
- name: Send status email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ requester_email }}"
subject: "Result of request {{ body_service_name }} : {{ perform }} user(s) in {{ group }}"
body: "{{ mail_body }}"
#body: "{{ lookup('file', '/var/www/release.txt') }}" # not using files due to WSL/ANSIBLE_RUNNER virtual environments
#when: hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true'
when: (hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true') and not self_service_user
listen:
- topic_nochange
- topic_add
- topic_remove
- name: Send customer add / remove email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ email_recipients }}"
#subject: "Cloudforms - Result of {{ body_service_name }}"
subject: "Welcome to the {{ body_service_name_noreqid }}"
# no attachments, cloudforms ansible-runner has trouble using directory paths
# we cant use file filter with binary files such as images
# instead our html body should reference the logo @ https://www.nottingham.ac.uk/SiteElements/Images/Base/logo.png
#attach: 'templates/{{ template_prefix }}/images/logo.png'
subtype: html
body: "{{ customer_change_mail_body }}"
when: (enable_customer_email == 'true' and email_recipients | length >0) and template_prefix != 'default'
listen:
- topic_add
- topic_remove
- name: Send customer nochange add email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ email_recipients_no_action }}"
subject: "Welcome to the {{ body_service_name_noreqid }}"
subtype: html
body: "{{ customer_nochange_add_mail_body }}"
#when: (enable_customer_email == 'true' and email_recipients_no_action | length >0) and template_prefix != 'default'
when: ((enable_customer_email == 'true' and email_recipients_no_action | length >0) and template_prefix != 'default') and state == 'present'
listen:
- topic_nochange
# commit message "disable customer email for scenario: no change with delete action"
# quick change so the customer nochange email (no longer being rendered with create/delete value of the perform variable) is only sent if a user is being added and is already in a group
# there is no nochange email for a user who is being removed from a group who is already not a member
# we likely dont want to fail on this condition when doing lookup on AD as other users still want to be processed (we only fail on invalid users for safety)
# is a customer nochange remove email useful? probably not
# Clarify with Mike
# - name: Send email
# debug:
# msg: "{{ mail_body }}"
# listen:
# - topic_nochange
# - topic_add
# - topic_remove
- name: Condition no change
debug:
msg:
- "no changes made to group {{ group }}"
- ---------------------------------------
- "users that had no action taken due to already being {{ state }} in group"
- ---------------------------------------
- "{{ user_no_action }}"
- ---------------------------------------
- "members of group {{ group }}"
- ---------------------------------------
- "{{ result.members }}"
- ---------------------------------------
listen: topic_nochange
- name: Condition user(s) added
debug:
msg:
- "user(s) added to group {{ group }}"
- ---------------------------------------
- "{{ result.added }}"
- ---------------------------------------
- "users that had no action taken due to already being {{ state }} in group"
- ---------------------------------------
- "{{ user_no_action }}"
- ---------------------------------------
- "members of group {{ group }}"
- ---------------------------------------
- "{{ result.members }}"
- ---------------------------------------
listen: topic_add
- name: Condition user(s) removed
debug:
msg:
- "user(s) removed from group {{ group }}"
- ---------------------------------------
- "{{ result.removed }}"
- ---------------------------------------
- "users that had no action taken due to already being {{ state }} in group"
- ---------------------------------------
- "{{ user_no_action }}"
- ---------------------------------------
- "members of group {{ group }}"
- ---------------------------------------
- "{{ result.members }}"
- ---------------------------------------
listen: topic_remove
# may need to test verbosity levels with handlers for clean log output in cloudforms, example of using verbosity level
#
# - name: Print Results
# debug:
# var: result
# verbosity: 0

View File

@ -0,0 +1,21 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
user(s) added to group {{ group }};
{% for entry in result.added %}
{{ entry }}
{% endfor %}
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}
members of group {{ group }}, current members include;
{% for entry in result.members %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,11 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
invalid AD user(s) {{ perform }} in group {{ group }};
{% for entry in no_aduser %}
{{ entry }}
{% endfor %}
job failed.

View File

@ -0,0 +1,17 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
no changes made to group {{ group }} with {{ perform }} operation
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}
members of group {{ group }}, current members include;
{% for entry in result.members %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,21 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
user(s) removed from group {{ group }};
{% for entry in result.removed %}
{{ entry }}
{% endfor %}
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}
members of group {{ group }}, current members include;
{% for entry in result.members %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,211 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<meta content="width=device-width" name="viewport"/>
<!--[if !mso]><!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<!--<![endif]-->
<title></title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 520px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
img.fullwidth,
img.fullwidthOnMobile {
max-width: 100% !important;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
.video-block {
max-width: none !important;
}
.mobile_hide {
min-height: 0px;
max-height: 0px;
max-width: 0px;
display: none;
overflow: hidden;
font-size: 0px;
}
.desktop_hide {
display: block !important;
max-height: none !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" class="nl-container" role="presentation" style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #FFFFFF; width: 100%;" valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:#FFFFFF"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 500px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px"><tr class="layout-full-width" style="background-color:transparent"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color:transparent;width:500px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12" style="min-width: 320px; max-width: 500px; display: table-cell; vertical-align: top; width: 500px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="left" class="img-container left autowidth" style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img alt="Alternate text" border="0" class="left autowidth" src="https://www.nottingham.ac.uk/SiteElements/Images/Base/logo.png" style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 243px; display: block;" title="Alternate text" width="243"/>
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 24px;">
<p style="font-size: 18px; line-height: 2; word-break: break-word; mso-line-height-alt: 36px; margin: 0;"><span style="font-size: 18px;">Welcome to the Automated Transcription Service</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">You have been given permission to use the Automated Transcription Service.</span></p>
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">Please login here with your University username and password:</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 24px;">
<p style="font-size: 14px; line-height: 2; word-break: break-word; mso-line-height-alt: 28px; margin: 0;"><a href="https://autotranscription.nottingham.ac.uk" rel="noopener" style="text-decoration: underline; color: #0068A5;" target="_blank">https://autotranscription.nottingham.ac.uk</a></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;">Please remember:</p>
<ul>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">Usage is charged per hour of audio and one month in arrears to a University project code</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">Audio files deleted immediately after transcription and transcripts after 90 days</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">To comply with General Data Protection Regulation (GDPR) you must tell your participants (e.g. in a Participant Information sheet) that the University of Nottinghams Automated Transcription Service is being used to process audio of their speech</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">In many cases the service can reach accuracy levels of 70-99%, but factors such as background noise and recording quality have a significant impact</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">The service can only transcribe English-language audio</li>
</ul>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;">If you are unsure about whether the Automated Transcription Service is the correct tool for your research, please contact a Digital Research Specialist.</p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;"><a href="https://uniofnottm.sharepoint.com/sites/DigitalResearch/SitePages/Automated-Transcription.aspx" rel="noopener" style="text-decoration: underline; color: #0068A5;" target="_blank">More information about the service, including video guides, comprehensive FAQs and the service usage policy</a></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">Best wishes,</span><br/><span style="font-size: 14px;">Digital Research</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View File

@ -0,0 +1,211 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<meta content="width=device-width" name="viewport"/>
<!--[if !mso]><!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<!--<![endif]-->
<title></title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 520px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
img.fullwidth,
img.fullwidthOnMobile {
max-width: 100% !important;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
.video-block {
max-width: none !important;
}
.mobile_hide {
min-height: 0px;
max-height: 0px;
max-width: 0px;
display: none;
overflow: hidden;
font-size: 0px;
}
.desktop_hide {
display: block !important;
max-height: none !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" class="nl-container" role="presentation" style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #FFFFFF; width: 100%;" valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:#FFFFFF"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 500px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px"><tr class="layout-full-width" style="background-color:transparent"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color:transparent;width:500px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12" style="min-width: 320px; max-width: 500px; display: table-cell; vertical-align: top; width: 500px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="left" class="img-container left autowidth" style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img alt="Alternate text" border="0" class="left autowidth" src="https://www.nottingham.ac.uk/SiteElements/Images/Base/logo.png" style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 243px; display: block;" title="Alternate text" width="243"/>
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 24px;">
<p style="font-size: 18px; line-height: 2; word-break: break-word; mso-line-height-alt: 36px; margin: 0;"><span style="font-size: 18px;">Welcome to the Automated Transcription Service</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">You already have permission to use the Automated Transcription Service.</span></p>
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">Please login here with your University username and password:</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 24px;">
<p style="font-size: 14px; line-height: 2; word-break: break-word; mso-line-height-alt: 28px; margin: 0;"><a href="https://autotranscription.nottingham.ac.uk" rel="noopener" style="text-decoration: underline; color: #0068A5;" target="_blank">https://autotranscription.nottingham.ac.uk</a></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;">Please remember:</p>
<ul>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">Usage is charged per hour of audio and one month in arrears to a University project code</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">Audio files deleted immediately after transcription and transcripts after 90 days</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">To comply with General Data Protection Regulation (GDPR) you must tell your participants (e.g. in a Participant Information sheet) that the University of Nottinghams Automated Transcription Service is being used to process audio of their speech</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">In many cases the service can reach accuracy levels of 70-99%, but factors such as background noise and recording quality have a significant impact</li>
<li style="font-size: 14px; line-height: 1.2; mso-line-height-alt: 17px;">The service can only transcribe English-language audio</li>
</ul>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;">If you are unsure about whether the Automated Transcription Service is the correct tool for your research, please contact a Digital Research Specialist.</p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;"><a href="https://uniofnottm.sharepoint.com/sites/DigitalResearch/SitePages/Automated-Transcription.aspx" rel="noopener" style="text-decoration: underline; color: #0068A5;" target="_blank">More information about the service, including video guides, comprehensive FAQs and the service usage policy</a></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">Best wishes,</span><br/><span style="font-size: 14px;">Digital Research</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View File

@ -0,0 +1,175 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<meta content="width=device-width" name="viewport"/>
<!--[if !mso]><!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<!--<![endif]-->
<title></title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 520px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
img.fullwidth,
img.fullwidthOnMobile {
max-width: 100% !important;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
.video-block {
max-width: none !important;
}
.mobile_hide {
min-height: 0px;
max-height: 0px;
max-width: 0px;
display: none;
overflow: hidden;
font-size: 0px;
}
.desktop_hide {
display: block !important;
max-height: none !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" class="nl-container" role="presentation" style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #FFFFFF; width: 100%;" valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:#FFFFFF"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid" style="Margin: 0 auto; min-width: 320px; max-width: 500px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px"><tr class="layout-full-width" style="background-color:transparent"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color:transparent;width:500px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12" style="min-width: 320px; max-width: 500px; display: table-cell; vertical-align: top; width: 500px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="left" class="img-container left autowidth" style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img alt="Alternate text" border="0" class="left autowidth" src="https://www.nottingham.ac.uk/SiteElements/Images/Base/logo.png" style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 243px; display: block;" title="Alternate text" width="243"/>
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">You have been removed from the Automated Transcription Service.</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="font-size: 14px; line-height: 1.2; word-break: break-word; mso-line-height-alt: 17px; margin: 0;"><a href="https://uniofnottm.sharepoint.com/sites/DigitalResearch/SitePages/Automated-Transcription.aspx" rel="noopener" style="text-decoration: underline; color: #0068A5;" target="_blank">More information about the service</a></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div style="color:#555555;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;line-height:1.2;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div style="line-height: 1.2; font-size: 12px; color: #555555; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; mso-line-height-alt: 14px;">
<p style="line-height: 1.2; word-break: break-word; font-size: 14px; mso-line-height-alt: 17px; margin: 0;"><span style="font-size: 14px;">Best wishes,</span><br/><span style="font-size: 14px;">Digital Research</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View File

@ -0,0 +1,15 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
user(s) added to group {{ group }};
{% for entry in result.added %}
{{ entry }}
{% endfor %}
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,11 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
invalid AD user(s) {{ perform }} in group {{ group }};
{% for entry in no_aduser %}
{{ entry }}
{% endfor %}
job failed.

View File

@ -0,0 +1,11 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
no changes made to group {{ group }} with {{ perform }} operation
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,15 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
user(s) removed from group {{ group }};
{% for entry in result.removed %}
{{ entry }}
{% endfor %}
users that had no action taken due to already being {{ state }};
{% for entry in user_no_action %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,111 @@
# Run on command line with -e for override variables or from Cloudforms with parameters, use comma delimiter when specifing multiple users:
#
# command line run with no email (not via cloudforms)
#
# ansible-playbook adgroup.yml -e 'groupmembers="tseed,swright" \
# group=Project \
# perform=delete \
# ad_host=WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL \
# ad_user="administrator@NETAPPSIM.LOCAL" \
# ad_pass="Password0" \
# from_email=tseed@ocf.co.uk \
# enable_requester_email=false \
# enable_customer_email=false \
# api_user=dummy \
# api_pass=dummy'
#
# command line run with email and non default customer email templates (not via cloudforms)
#
# ansible-playbook adgroup.yml -e 'groupmembers="tseed,swright" \
# group=Project \
# perform=delete \
# ad_host=WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL \
# ad_user=administrator \
# ad_pass="Password0" \
# from_email="noreply@cloudforms" \
# enable_requester_email=true \
# enable_customer_email=true \
# smtp_relay=192.168.101.240 \
# smtp_port=25 \
# template_prefix=transcription \
# requester_email=tseed@ocf.co.uk \
# api_user=dummy \
# api_pass=dummy'
#
# command line run with email, non default customer email templates, a single groupmember and spoof self service mode enabled (replicate cloudforms provisioning for the requester)
# this will disable the enable_requester_email
#
# ansible-playbook adgroup.yml -e 'groupmembers="tseed" \
# group=Project \
# perform=delete \
# ad_host=WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL \
# ad_user=administrator \
# ad_pass="Password0" \
# from_email="noreply@cloudforms" \
# enable_requester_email=true \
# enable_customer_email=true \
# smtp_relay=192.168.101.240 \
# smtp_port=25 \
# template_prefix=transcription \
# requester_email=tseed@ocf.co.uk \
# api_user=dummy \
# api_pass=dummy \
# spoof_self_service=true'
#
# requester email templates are prefixed mail-, customer email templates customer-, customer emails are in html format
# customers will only recieve an add/remove email when this action has been performed upon their account
# when template_prefix is omitted the value is set to default and default email templates used, in this scenario customer emails are not sent disregarding the parameter enable_customer_email
#
# variables are evaluated for the term 'placeholder', when found the playbook will exit
# variables when evaluated for the term 'placeholder' have their output supressed to ensure the cloudforms log doesnt include sensitive parameters such as passwords
# to assist with debug pass an optional paramter of 'suppress_vars_output=false'
#
---
# variables to create in-memory inventory of the AD server, notice these variables are in the winrm format that would be under the entry [<hostgroup>:vars] for an inventory file
# these variables can also be passed with as parameters by cloudforms and will override any instances in this file ( cloudforms uses ansible --extra-vars)
# any variables set as 'placeholder' must be passed as parameters at runtime else exit
#
ad_host: placeholder # active directory server, when using kerberos (with requisite resolv.conf entry) this must be a fqdn
ad_user: placeholder # AD service account capable of manipulating group membership
ad_pass: "placeholder" # AD service account password
ad_connection: winrm
ad_winrm_transport: kerberos
ad_winrm_kinit_mode: managed # allow ansible to manage own kerberos token, SSSD manages when set to manual
ad_winrm_message_encryption: auto # can be set to always, depends on ad server profile
ad_port: 5986 # 5985/http for non https transport, UON on-prem use 5986/https
ad_winrm_scheme: https # UON on-prem use 5986/https
ad_winrm_server_cert_validation: ignore
# set to false for task "Fail Where Requisite Vars Not Set" to output all variables to assist debug
suppress_vars_output: true
# control email recipients
enable_requester_email: false
enable_customer_email: false
# control self service mode, this is to replicate cloudforms bahaviour where the requester populates the groupmembers parameter only with itself
# this mode effectively disables status emails even if enable_requester_email: true is passed as a parameter
spoof_self_service: false
# smtp server
smtp_relay: smtp.nottingham.ac.uk
smtp_port: 25
# email template path, this value changes the template path to ./templates/<value>/ , the value is set to default when not specified as a parameter in cloudforms
# default templates list all usable variables that maybe used in a template
# when changing the prefix, ensure a matching folder name exists in the templates folder containing file; mail-add.j2, mail-invalid.j2, mail-nochange.j2, mail-remove.j2
template_prefix: "default"
# mandatory parameters for actions and targets
perform: placeholder # should be "create" or "delete"
groupmembers: placeholder # users or groups to be added into group, this is a comma separated list, single entries are converted to a list with a single item
group: placeholder # group, single group accepted
from_email: placeholder # should be a service account address such as donotreply@nottingham.ac.uk
# Cloudforms API
api_user: placeholder
api_pass: placeholder
# groups for use in UON tiles
# UI-Transcription-Live
# UI-High-Performance-Windows

View File

@ -0,0 +1,57 @@
## What is this playbook?
It provisions GPFS storage shared over samba, it is designed to be run parameterised from cloudforms.
The playbook was tested on a development build of a GPFS single node cluster with CES service, the associated ARM template provisions an Azure instance with compatible IP scheme: Ansible Playbooks/ARM_templates/dev/rev1/spectrum_scale_ARM_v2.json
This playbook is functional but unfinished requiring input validation for cloudforms parameterise, better error condition reporting and customised UoN branded HTML customer notification email templates.
Time was called when the production GPFS CES host was not joined to the Active Directory domain in a way that would return windows SID and enable domain user logon to the resultant samba share, RCF2307 is planned to be implemented.
## The playbook demonstrates
Checks cloudforms environment and changes the name of the service to include the request ID to uniquely identify what has been ordered.
Check the AD users/groups provided for the share permissions are valid.
Builds a list of users/groups who will have access to the samba share.
Builds a list of users is nested groups and looks up from AD their associated email address to be used in access notification emails.
Creates a GPFS fileset via API.
Creates a GPFS fileset quota via API.
Creates a GPFS samba share via API.
Creates a GPFS samba share ACL via API.
Reports results via console and email.

View File

@ -0,0 +1,180 @@
---
- name: multi loop
block:
- name: Reset loop variables # without unsetting infinte loops will occur
set_fact:
group_present: false
find_groups: []
#find_users: [] # we dont want to reset this var, this will append on each loop until exit (unexpected behaviour but works)
find_group_members_tidy: []
group_members: []
member_type: []
member_attributes: []
# - name: Inspect dummy host object_attributes
# debug:
# msg: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
- name: Filter groups into a list - CHANGED
set_fact:
#find_groups: "{{ find_groups | default([]) }} + ['{{ item.name }}']"
find_groups: "{{ find_groups | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
with_items: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
when: item.type == 'Group'
- name: Append users to list - CHANGED
set_fact:
#find_users: "{{ find_users | default([]) }} + ['{{ item.name }}']"
find_users: "{{ find_users | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
with_items: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
when: item.type == 'Person'
# - debug:
# msg:
# - "{{ find_groups }}"
# - "{{ find_users }}"
# when: find_groups is defined
####### working
# - name: Query group members - CHANGED
# #win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne().Properties.member
# win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.member
# register: find_group_members_result
# with_items: "{{ find_groups }}"
# #when: find_groups is defined
# when: find_groups is defined and item.role == 'member'
# - name: Query group members type owner - NEW
# #win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne().Properties.member
# win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.member
# register: find_group_members_result_type_owner
# with_items: "{{ find_groups }}"
# when: find_groups is defined and item.role == 'owner' # instead of owner/member this could be RW/RWX
####### working
- name: Query group members
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.member
register: find_group_members_result
with_items: "{{ find_groups }}"
when: find_groups is defined
# - debug:
# msg: "{{ find_group_members_result }}"
# when: find_group_members_result is defined
####### working
# - name: Tidy group members into list - CHANGED - stuck here
# set_fact:
# #group_members: "{{ group_members | default([]) + [item.split(',')[0].split('CN=')[1]] }}"
# group_members: "{{ group_members | default([]) + [dict(name=item.split(',')[0].split('CN=')[1], role='member')] }}"
# with_items: "{{ find_group_members_result | json_query(jmesquery) }}"
# vars:
# jmesquery: "results[].stdout_lines"
# #when: find_group_members_result.results[0].stdout | length >0 # bad check
# when: find_group_members_result is defined and (item is defined and item | length >0)
# - name: Tidy group members type owner into list - NEW
# set_fact:
# group_members: "{{ group_members | default([]) + [dict(name=item.split(',')[0].split('CN=')[1], role='owner')] }}"
# with_items: "{{ find_group_members_result_type_owner | json_query(jmesquery) }}"
# vars:
# jmesquery: "results[].stdout_lines"
# when: find_group_members_result_type_owner is defined and (item is defined and item | length >0)
####### working
- name: Tidy group members into dict of names and owner
set_fact:
find_group_members_tidy: '{{ find_group_members_tidy | default([]) + [dict(name=item.name, role=item.role)] }}'
with_items: "{{ find_group_members_result | json_query(jmesquery) }}"
vars:
jmesquery: "results[].{role: @.item.role, name: @.stdout_lines}"
#entry: "{{ item.role }}"
- name: Tidy group members into dict of name and owner
set_fact:
group_members: "{{ group_members | default([]) + [dict(name=item.1.split(',')[0].split('CN=')[1], role=item.0.role)] }}"
with_subelements:
- "{{ find_group_members_tidy }}"
- name
# - name: Tidy group members type owner into list - NEW (sort into dictionary with role as key)
# set_fact:
# group_members: '{{ group_members | default([]) + [dict(entry)] }}' # works cool
# with_items: "{{ find_group_members_result | json_query(jmesqueryC) }}"
# vars:
# jmesqueryC: "results[].{role: @.item.role, name: @.stdout_lines}"
# entry: '{"{{ item.role }}":"{{ item.name }}"}'
# - debug:
# msg: "{{ group_members }}"
# when: group_members is defined
- name: Check AD object is user or group - CHANGED
#win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne().Properties.objectcategory
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.objectcategory
register: member_type_result
with_items: "{{ group_members }}"
when: group_members is defined
# - debug:
# msg: "{{ member_type_result }}"
# when: group_members is defined
- name: Build list of AD object type
set_fact:
member_type: "{{ member_type | default([]) + [(item.stdout.split(',')[0].split('CN=')[1])] }}"
with_items: "{{ member_type_result.results }}"
when: group_members is defined
# - debug:
# msg: "{{ member_type }}"
# when: group_members is defined
- name: Build dict of object names and types - CHANGED
set_fact:
#member_attributes: "{{ member_attributes | default([]) + [ dict(name=item[0], type=item[1]) ] }}"
member_attributes: "{{ member_attributes | default([]) + [ dict(name=item[0].name, role=item[0].role, type=item[1]) ] }}" # effectively adding a new positional field from the list to the dict
loop: "{{ group_members|zip(member_type)|list }}"
when: group_members is defined
# - debug:
# msg: "{{ member_attributes }}"
# when: group_members is defined
- name: Reset/Add variable object_attributes to dummy host for next loop
add_host:
name: "DUMMY_HOST"
object_attributes: "{{ member_attributes }}"
when: member_attributes is defined
- name: Set flag to notify there are nested groups
set_fact:
group_present: true
with_items: "{{ member_attributes }}"
when: member_attributes is defined and item.type == 'Group'
#when: member_attributes is defined and item.type == 'GroupA' # used to break loop with test for no group_present
- name: Nested group present?
fail:
msg: Nested group detected
when: group_present
# the following tasks only run on the last run of the loop where there are no more groups detected
# append the users from the last run of the loop to find_users
# set find_users as a DUMMY_HOST variable to be retrieved from the calling script - this is how you pass variables back from different plays also works for include_tasks
- name: Append users to list - CHANGED
set_fact:
#find_users: "{{ find_users | default([]) + [dict(name=item.name, type=member)] }}"
find_users: "{{ find_users | default([]) + [dict(name=item.name, type=item.type, role=item.role)] }}"
with_items: "{{ member_attributes }}"
when: item.type == 'Person'
- name: Add users to variable find_users for dummy host on final loop
add_host:
name: "DUMMY_HOST"
find_users: "{{ find_users }}"
when: find_users is defined
rescue:
- include_tasks: group_lookup.yml

View File

@ -0,0 +1,589 @@
---
# PLAY
# Gather cloudforms information and set service name
- name: Query automate workspace
hosts: localhost
gather_facts: False
tasks:
- set_fact:
endpoint: "{{ manageiq.api_url }}"
auth_token: "{{ manageiq.api_token }}"
request_id: "{{ (manageiq.request).split('/')[-1] }}"
service_id: "{{ (manageiq.service).split('/')[-1] }}"
user_id: "{{ (manageiq.user).split('/')[-1] }}"
when: manageiq is defined
# when users run ansible their manageiq auth token does not have sufficient rights to interact with the API, no combination of rights in a role for a non admin user are sufficient
# use the local admin credentials to get an auth token
- name: Get auth token
uri:
url: "{{ endpoint }}/api/auth"
validate_certs: no
method: GET
user: "{{ api_user }}"
password: "{{ api_pass }}"
status_code: 200
register: login
when: manageiq is defined
- set_fact:
auth_token: "{{ login.json.auth_token }}"
when: manageiq is defined
- name: Get requester user attributes
uri:
url: "{{ endpoint }}/api/users/{{ user_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: user
when: manageiq is defined
- set_fact:
requester_email: "{{ user.json.email }}"
when: manageiq is defined and user.json.email is not none # cloudforms admin user has no email address (unless set), this is a null field in json
- set_fact:
#requester_email: "{{ from_email }}" # from_email should be able to recieve mail via relay
requester_email: ucats@exmail.nottingham.ac.uk
when: requester_email is not defined
- set_fact:
requester_user: "{{ user.json.name }}"
when: manageiq is defined
# when run from the command line set a default requester user
- set_fact:
requester_user: "command line invocation"
when: manageiq is not defined
# use when we need to determine if this is a self service request - might not be used for this playbook
# - name: get AD account name
# set_fact:
# requester_user_ad: "{{ (user.json.email).split('@')[0] }}"
# when: manageiq is defined
- name: get service
uri:
url: "{{ endpoint }}/api/services/{{ service_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: service
when: manageiq is defined
- set_fact:
service_name: "{{ service.json.name }}"
new_service_name: "{{ service.json.name }} {{ request_id }}"
when: manageiq is defined
- set_fact:
service_name: "command line invocation"
new_service_name: "command line invocation"
when: manageiq is not defined
- name: set service name
uri:
url: "{{ endpoint }}/api/services/{{ service_id }}"
validate_certs: no
method: POST
headers:
X-Auth-Token: "{{ auth_token }}"
body_format: json
body: { "action" : "edit", "resource" : { "name" : "{{ new_service_name }}" }}
status_code: 200, 204
register: service
when: manageiq is defined
# PLAY
# Validate parameters passed to script from cloudforms
- name: Script input Validation
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
vars:
ownerlist: []
groupmemberslist: []
groupmemberslistvalidate: []
tasks:
# add task to check + fail for any variables with name placeholder here
- name: Convert owner to list
set_fact:
ownerlist: "{{ ownerlist }} + [ '{{ item }}' ]"
with_items: "{{ owner }}"
- name: Split groupmembers parameter on , delimiter
set_fact:
groupmemberslist: "{{ groupmemberslist }} + [ '{{ item }}' ]"
with_items: "{{ groupmembers.split(',') }}"
- name: Remove empty fields from groupmembers parameter
set_fact:
groupmemberslistvalidate: "{{ groupmemberslistvalidate }} + [ '{{ item }}' ]"
when: item | length != 0
with_items: "{{ groupmemberslist }}"
# PLAY
# Add winrm host used for AD querys to inventory
- name: Build inventory for AD server
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
tasks:
- name: Add host entry for adserver
add_host: >
name=adserver
groups=windows
ansible_host="{{ ad_host }}"
# PLAY
# Query winrm to validate AD user/group exist, build dict for share ACL and dict of all user/email from a recursively search of user/group
- name: Check AD user exists
hosts: adserver
gather_facts: false
vars_files:
- vars/main.yml
vars:
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
ownerlist: "{{ hostvars['localhost']['ownerlist'] }}"
groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}"
ownermembers: "{{ ownerlist + groupmemberslistvalidate }}"
no_aduser: []
object_type: []
tasks:
- name: Add connectivity variables for adserver
set_fact:
ansible_user: "{{ ad_user }}"
ansible_password: "{{ ad_pass }}"
ansible_connection: "{{ ad_connection }}"
ansible_winrm_transport: "{{ ad_winrm_transport }}"
ansible_winrm_kinit_mode: "{{ ad_winrm_kinit_mode }}"
ansible_winrm_message_encryption: "{{ ad_winrm_message_encryption }}"
ansible_port: "{{ ad_port }}"
ansible_winrm_scheme: "{{ ad_winrm_scheme }}"
ansible_winrm_server_cert_validation: "{{ ad_winrm_server_cert_validation }}"
# ansible_winrm_operation_timeout_sec: 60
# ansible_winrm_read_timeout_sec: 60
- name: Check AD user/group exists
win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne()
register: command_result
with_items:
# - "{{ ownerlist }}"
# - "{{ groupmemberslistvalidate }}"
- "{{ ownermembers }}"
- name: Flag fail where AD user/group not exist
set_fact:
no_aduser: "{{ no_aduser + [item.item] }}"
when: item.stdout | length == 0
with_items: "{{ command_result.results }}"
changed_when: true
notify: topic_noad
- name: Check AD object is user or group
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.item }})").FindOne().Properties.objectcategory
register: object_result
with_items: "{{ command_result.results }}"
when: no_aduser | length == 0
- name: Build list of AD object type
set_fact:
object_type: "{{ object_type + [(item.stdout.split(',')[0].split('CN=')[1])] }}" # faster than regex
with_items: "{{ object_result.results }}"
when: no_aduser | length == 0
# cheat, the first entry in the list ownermember (user or group) is the owner where we merge ownerlist + groupmemberslistvalidate
# for this to work the owner list must have only one entry
# to use this logic for groups of objects (i.e rwx, rw, r groups), we will need to merge a dict with fields name, role and the list object_attributes
- name: Build dict of object names, types and roles positionally from list with object name and list with object type
set_fact:
object_attributes: "{{ object_attributes | default([]) + [dict(name=item[0], type=item[1], role='owner' if object_attributes is undefined else 'member') ] }}"
loop: "{{ ownermembers|zip(object_type)|list }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ object_attributes }}"
# when: no_aduser | length == 0
# recursive loop control using include_tasks and block rescue behaviour, https://github.com/ansible/ansible/issues/46203
# write/access variables between plays using dummy host variables, https://www.unixarena.com/2019/05/passing-variable-from-one-playbook-to-another-playbook-ansible.html/
- name: Register dummy host with variable object_attributes
add_host:
name: "DUMMY_HOST"
object_attributes: "{{ object_attributes }}"
when: no_aduser | length == 0
- name: Find all group members
include_tasks: group_lookup.yml
when: no_aduser | length == 0
# - name: Inspect all users who will require email notification
# debug:
# msg: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
# when: no_aduser | length == 0
- name: Import dummy host variable from group_lookup.yml
set_fact:
unique_users: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
when: no_aduser | length == 0
- name: Get unique name/role/type entries, remove duplicate entries that may arise from group nesting
set_fact:
unique_users: "{{ unique_users | unique }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ unique_users }}"
- name: Get owner names into a list
set_fact:
owner_users: "{{ owner_users | default([]) + [item.name] }}"
with_items: "{{ unique_users }}"
when: no_aduser | length == 0 and item.role == 'owner'
- name: Get member names into a list
set_fact:
member_users: "{{ member_users | default([]) + [item.name] }}"
with_items: "{{ unique_users }}"
when: no_aduser | length == 0 and item.role == 'member'
- name: Get names common in both lists
set_fact:
common_users: "{{ owner_users | intersect(member_users) }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ common_users }}"
# when: no_aduser | length == 0
# KEEP THIS
# this is a dedupe task where duplicate users exist one with role owner and one with role member, the user with role member is removed
# this isnt too helpful in gpfs as the owner attribute is the unix owner of the fileset mount point not a windows ACL
# the logic is useful in a permissions scenario - example multiple entries for a user with rwx and r-- permissions, we want to ensure only the rwx role is used
# - name: Remove member entries where competing owner entry exists
# set_fact:
# email_users: "{{ email_users | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
# with_items: "{{ unique_users }}"
# when: no_aduser | length == 0 and (item.name not in common_users or (item.name in common_users and item.role == 'owner'))
- set_fact:
email_users: "{{ unique_users }}"
when: no_aduser | length == 0
# final deduplicated dict to send appropriate class of email to users
# - debug:
# msg: "{{ email_users }}"
# when: no_aduser | length == 0
- name: Get member email address from AD
win_shell: Get-ADUser {{ item.name }} -Properties mail | Select-Object -ExpandProperty mail
register: email_result
with_items: "{{ email_users }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ email_result }}"
# when: no_aduser | length == 0
# this will crash out where customer account has no associated email, UoN are very consistent with account creation and adding email - needs logic for empty elements
- name: Get member emails into a list
set_fact:
email_address: "{{ email_address | default([]) + [item.stdout_lines[0]] }}"
with_items: "{{ email_result.results }}"
when: no_aduser | length == 0
- name: Build dict of object name, role, type and email
set_fact:
user_dict: "{{ user_dict | default([]) + [dict(name=item[0].name, role=item[0].role, type=item[0].type, email=item[1])] }}" # adding a new positional field from the list to the dict
loop: "{{ email_users|zip(email_address)|list }}"
when: no_aduser | length == 0
# COMMENT OUT
- debug:
msg: "{{ user_dict }}"
when: no_aduser | length == 0
handlers:
- name: Build invalid user(s) email body
set_fact:
mail_body: "{{ lookup('template', 'templates/mail-invalid.j2') }}"
delegate_to: localhost
listen:
- topic_noad
- name: Send status email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ requester_email }}"
subject: "Result of request {{ body_service_name }}"
body: "{{ mail_body }}"
delegate_to: localhost
when: requester_email is defined and enable_requester_email
listen:
- topic_noad
- name: Condition fail on invalid AD user
debug:
msg:
- "invalid AD user(s)/groups(s)"
- ---------------------------------------
- "{{ no_aduser }}"
- ---------------------------------------
delegate_to: localhost
listen:
- topic_noad
- name: Hard exit
fail:
listen:
- topic_noad
# PLAY
# Create GPFS fileset with quota, samba export and accociated ACLs
- name: GPFS create fileset and samba export with quota
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
- vars/requests.yml
vars:
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
tasks:
# task here to work out quota size as a parameter
- name: Check samba share exists
include_tasks: tasks/checkfilesetsambaexport.yml
- name: Check fileset exists
include_tasks: tasks/checkfileset.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
- name: Create fileset
include_tasks: tasks/createfileset.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
- name: Create fileset quota
include_tasks: tasks/createfilesetquota.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
- name: Create samba share
include_tasks: tasks/createsambaexport.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
# if the ACL apply fails the loop terminates, users following a failed user ACL will not be processed, this shouldnt happen but there is no control for this
# would be good to check share exists already like the following remove task, would help if the script needs to update ACL
- name: Create samba share ACL
include_tasks: tasks/createsambaacl.yml
vars:
ad_object: "{{ item.name }}"
with_items: "{{ hostvars['adserver']['object_attributes'] }}"
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined and item.role == 'member' # remember KEEP THIS comment under hosts: adserver
- name: Remove samba share ACL
include_tasks: tasks/removesambaacl.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
- name: List samba share ACL
include_tasks: tasks/listsambaacl.yml
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
- name: Report storage failure
set_fact:
storage_fail: "{{ hostvars['DUMMY_HOST']['storage_fail'] }}"
when: hostvars['DUMMY_HOST']['storage_fail'] is defined
changed_when: true
notify: topic_fail
# - name: Get members for output
# set_fact:
# notify_members: "{{ notify_members | default([]) + [dict(name=item.name, email=item.email)]}}"
# with_items: "{{ hostvars['adserver']['user_dict'] }}"
# when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
# - name: Report ACL entry
# set_fact:
# acl_entry: "{{ hostvars['DUMMY_HOST']['acl_entry'] }}"
# when: hostvars['DUMMY_HOST']['acl_entry'] is defined
# changed_when: true
# notify: topic_pass
handlers:
- name: Build fail on storage provision email body
set_fact:
mail_body: "{{ lookup('template', 'templates/mail-storage-fail.j2') }}"
listen:
- topic_fail
- name: Send fail email
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ requester_email }}"
subject: "Result of request {{ body_service_name }}"
body: "{{ mail_body }}"
when: requester_email is defined and enable_requester_email
listen:
- topic_fail
- name: Condition fail on provision storage
debug:
msg:
- "provisioning storage failure"
- ---------------------------------------
- "{{ storage_fail }}"
- ---------------------------------------
listen:
- topic_fail
- name: Hard exit
fail:
listen:
- topic_fail
# PLAY
# Console and email output of results
- name: Report results
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
vars:
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
tasks:
# - name: Report storage failure
# set_fact:
# storage_fail: "{{ hostvars['DUMMY_HOST']['storage_fail'] }}"
# when: hostvars['DUMMY_HOST']['storage_fail'] is defined
# changed_when: true
# notify: topic_fail
- name: Get members name/email for notification requester email body
set_fact:
notify_members: "{{ notify_members | default([]) + [dict(name=item.name, email=item.email)]}}"
with_items: "{{ hostvars['adserver']['user_dict'] }}"
when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
- name: Get member email to list for email module
set_fact:
notify_customers: "{{ notify_customers | default([]) + [item.email]}}"
with_items: "{{ hostvars['adserver']['user_dict'] }}"
when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
- name: Report ACL entry
set_fact:
acl_entry: "{{ hostvars['DUMMY_HOST']['acl_entry'] }}"
when: hostvars['DUMMY_HOST']['acl_entry'] is defined
changed_when: true
notify: topic_pass
handlers:
- name: Build requester storage provision email body
set_fact:
mail_body: "{{ lookup('template', 'templates/mail-storage-requester.j2') }}"
listen:
- topic_pass
- name: Send requester storage provision email body
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ requester_email }}"
subject: "Result of request {{ body_service_name }}"
body: "{{ mail_body }}"
when: requester_email is defined and enable_requester_email
listen:
- topic_pass
- name: Build member storage provision email body
set_fact:
mail_body: "{{ lookup('template', 'templates/mail-storage-customer.j2') }}"
listen:
- topic_pass
- name: Send member storage provision email body
mail:
host: "{{ smtp_relay }}"
port: "{{ smtp_port }}"
charset: utf-8
from: "{{ from_email }}"
to: "{{ notify_customers }}"
subject: "Result of request {{ body_service_name }}"
body: "{{ mail_body }}"
when: enable_customer_email
listen:
- topic_pass
- name: Report "{{ filesetName }}" sucessful creation and applied ACL
debug:
msg:
- "storage {{ filesetName }} sucessfully created"
- ------------------------------------------------------------------------------
- "access share @UNC path //{{ clusterapiIP }}/{{ filesetName }}"
- ------------------------------------------------------------------------------
- "applied ACL for {{ filesetName }}"
- ------------------------------------------------------------------------------
- "{{ acl_entry }}"
- ------------------------------------------------------------------------------
- "users with access to the share that will recieve email notification"
- ------------------------------------------------------------------------------
- "{{ notify_members }}"
listen:
- topic_pass
# TEST + BUILD
# need input placeholder validation
# need a maximum size of share as a parameter, this will enable different tiles/tag combinations for users/groups
# may change with netapp module - use a different play to report + email - is there any point? maybe with netapp + dual functionality (play for netapp, play for gpfs) - use some more handlers and the exisitng customer emails switch
# NOT UNTIL MORE REQUIREMENTS
# owner as a group - no need today - this is a unix permission, maybe best set to service_cloudforms or even local root of GPFS
# dont think i need to worry about 'you are already member of' regarding ACL's - we are creating new storage - no requirement to delete storage yet - can put in check it will help for update/add permissions
# nice jmespath check - clean
# - name: query results of output files exist on remote host
# set_fact:
# query_certs: "{{ stat_result | json_query(jmesquery) }}"
# vars:
# jmesquery: "results[?(@.stat.exists)].item"

View File

@ -0,0 +1,23 @@
---
- name: Check fileset exists
uri:
url: "{{ checkfilesetEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: GET
validate_certs: no
return_content: yes
force_basic_auth: yes
status_code: 200, 400
register: response
# - name: Report failure
# fail:
# msg: "{{ filesetName }} already exists, please choose another share name"
# when: response.json.status.code == 200
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "{{ filesetName }} fileset already exists, please choose another share name"
when: response.json.status.code == 200

View File

@ -0,0 +1,23 @@
---
- name: Check share exists
uri:
url: "{{ checkfilesetsambaexportEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: GET
validate_certs: no
return_content: yes
force_basic_auth: yes
status_code: 200, 400
register: response
# - name: Report failure
# fail:
# msg: "{{ filesetName }} already exists, please choose another share name"
# when: response.json.status.code == 200
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "{{ filesetName }} share already exists, please choose another share name"
when: response.json.status.code == 200

View File

@ -0,0 +1,34 @@
---
- name: Check job
uri:
url: "{{ checkjobEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: GET
validate_certs: no
return_content: yes
body_format: json
force_basic_auth: yes
register: jobresponse
until: jobresponse.json.jobs[0].status != 'RUNNING'
retries: 20
delay: 10
# - name: Report failure
# fail:
# msg:
# - "{{ fail_message }}"
# - "{{ jobresponse.json.jobs[0].result.stderr }}"
# when: jobresponse.json.jobs[0].status == 'FAILED'
# - name: Check job id and endpoint
# debug:
# msg:
# - "{{ jobId }}"
# - "{{ checkjobEndpoint }}"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "{{ fail_message }}, API output {{ jobresponse.json.jobs[0].result.stderr }}"
when: jobresponse.json.jobs[0].status == 'FAILED'

View File

@ -0,0 +1,43 @@
---
- name: Create fileset
uri:
url: "{{ createfilesetEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: POST
validate_certs: no
return_content: yes
body_format: json
body: "{{ createfileset }}"
force_basic_auth: yes
status_code: 202, 400
register: response
# - name: Report failure
# fail:
# msg: "Invalid request body"
# when: response.json.status.code == 400
# - set_fact:
# jobId: "{{ response.json.jobs[0].jobId }}"
# - name: Check Job
# include_tasks: checkjob.yml
# vars:
# failmessage: "Unable to create fileset"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "Create fileset invalid request body, please notify administrator"
when: response.json.status.code == 400
- set_fact:
jobId: "{{ response.json.jobs[0].jobId }}"
when: response.json.status.code != 400
- name: Check Job
include_tasks: checkjob.yml
vars:
fail_message: "Unable to create fileset"
when: response.json.status.code != 400

View File

@ -0,0 +1,43 @@
---
- name: Create fileset quota
uri:
url: "{{ createfilesetquotaEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: POST
validate_certs: no
return_content: yes
body_format: json
body: "{{ quotafileset }}"
force_basic_auth: yes
status_code: 202, 400
register: response
# - name: Report failure
# fail:
# msg: "Invalid request body"
# when: response.json.status.code == 400
# - set_fact:
# jobId: "{{ response.json.jobs[0].jobId }}"
# - name: Check Job
# include_tasks: checkjob.yml
# vars:
# failmessage: "Unable to create fileset quota"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "Create fileset quota invalid request body, please notify administrator"
when: response.json.status.code == 400
- set_fact:
jobId: "{{ response.json.jobs[0].jobId }}"
when: response.json.status.code != 400
- name: Check Job
include_tasks: checkjob.yml
vars:
fail_message: "Unable to create fileset quota"
when: response.json.status.code != 400

View File

@ -0,0 +1,47 @@
---
# GPFS AD connectivity/cache may not be able to return a user lookup from AD in a timely manner after a period of inactivity, until loop employed
- name: Create fileset samba export ACL
uri:
url: "{{ createsambaaclEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: PUT
validate_certs: no
return_content: yes
body_format: json
body: "{{ smbexportfilesetacl }}"
force_basic_auth: yes
status_code: 202, 400
register: response
until: response.json.status.code == 202
retries: 20
delay: 10
# - name: Report failure
# fail:
# msg: "Invalid request body"
# when: response.json.status.code == 400
# - set_fact:
# jobId: "{{ response.json.jobs[0].jobId }}"
# - name: Check Job
# include_tasks: checkjob.yml
# vars:
# failmessage: "Unable to apply ACL to share {{ filesetName }}"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "Create share ACL, invalid request body or GPFS->AD connectivity issue, please notify administrator"
when: response.json.status.code == 400
- set_fact:
jobId: "{{ response.json.jobs[0].jobId }}"
when: response.json.status.code != 400
- name: Check Job
include_tasks: checkjob.yml
vars:
fail_message: "Unable to apply ACL to share"
when: response.json.status.code != 400

View File

@ -0,0 +1,43 @@
---
- name: Create fileset samba export
uri:
url: "{{ createsambaexportEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: POST
validate_certs: no
return_content: yes
body_format: json
body: "{{ smbexportfileset }}"
force_basic_auth: yes
status_code: 202, 400
register: response
# - name: Report failure
# fail:
# msg: "Invalid request body"
# when: response.json.status.code == 400
# - set_fact:
# jobId: "{{ response.json.jobs[0].jobId }}"
# - name: Check Job
# include_tasks: checkjob.yml
# vars:
# failmessage: "Unable to create share"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "Create share invalid request body, please notify administrator"
when: response.json.status.code == 400
- set_fact:
jobId: "{{ response.json.jobs[0].jobId }}"
when: response.json.status.code != 400
- name: Check Job
include_tasks: checkjob.yml
vars:
fail_message: "Unable to create share"
when: response.json.status.code != 400

View File

@ -0,0 +1,28 @@
---
- name: List fileset samba export ACL
uri:
url: "{{ listsambaaclEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: GET
validate_certs: no
return_content: yes
force_basic_auth: yes
status_code: 200, 202, 400
register: response
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "List share ACL invalid request body, please notify administrator"
when: response.json.status.code == 400
- name: Register dummy host with variable acl_entry
add_host:
name: "DUMMY_HOST"
acl_entry: "{{ response.json.smbAclEntries }}"
when: response.json.status.code == 200
# - debug:
# msg:
# - "{{ response.json.smbAclEntries }}"

View File

@ -0,0 +1,70 @@
---
- name: Check fileset samba export default ACL
uri:
url: "{{ checksambaeveryoneaclEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: GET
validate_certs: no
return_content: yes
force_basic_auth: yes
status_code: 200, 400
register: check_response
# - name: Inspect remove ACL check
# debug:
# msg: "{{ check_response }}"
# GPFS AD connectivity/cache may not be able to return a user lookup from AD in a timely manner after a period of inactivity, until loop employed
- name: Remove fileset samba export default ACL
uri:
url: "{{ removesambaeveryoneaclEndpoint }}"
user: "{{ clusterUser }}"
password: "{{ clusterPassword }}"
method: DELETE
validate_certs: no
return_content: yes
force_basic_auth: yes
status_code: 202, 400
register: response
until: response.json.status.code == 202
retries: 20
delay: 10
when: check_response.json.status.code == 200
# - name: Inspect remove ACL check
# debug:
# msg: "{{ response }}"
# - name: Report failure
# fail:
# msg: "Invalid request body"
# when: response.json.status.code == 400
# - set_fact:
# jobId: "{{ response.json.jobs[0].jobId }}"
# - name: Check Job
# include_tasks: checkjob.yml
# vars:
# failmessage: "Unable to remove ACL for group Everyone for share {{ filesetName }}"
- name: Register dummy host with variable storage_fail
add_host:
name: "DUMMY_HOST"
storage_fail: "Remove share ACL invalid request body, please notify administrator"
#when: not response.skipped and response.json.status.code == 400
when: response.json.status.code == 400
- set_fact:
jobId: "{{ response.json.jobs[0].jobId }}"
#when: not response.skipped and response.json.status.code != 400
when: response.json.status.code != 400
- name: Check Job
include_tasks: checkjob.yml
vars:
fail_message: "Unable to remove 'group Everybody' ACL for share"
#when: not response.skipped and response.json.status.code != 400
when: response.json.status.code != 400

View File

@ -0,0 +1,11 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
invalid AD user(s);
{% for entry in no_aduser %}
{{ entry }}
{% endfor %}
job failed.

View File

@ -0,0 +1,11 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
you have been added to storage;
{{ filesetName }}
to access this share navigate to the following UNC path and provide university credentials;
//{{ clusterapiIP }}/{{ filesetName }}

View File

@ -0,0 +1,9 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
failed to provision storage;
{{ storage_fail }}
job failed.

View File

@ -0,0 +1,19 @@
Service request: {{ body_service_name }}
Requester: {{ body_requester_user }}
Result:
storage sucessfully provisioned;
{{ filesetName }}
to access this share navigate to the following UNC path and provide university credentials;
//{{ clusterapiIP }}/{{ filesetName }}
ACL applied to share;
{{ acl_entry }}
users with access to the share;
{{ notify_members }}

View File

@ -0,0 +1,69 @@
---
# CES ip/credentials
clusterapiIP: "51.132.24.66"
clusterUser: ocfadmin
clusterPassword: mnBMghZLWg63Kge2
# CES filesystem mount attributes
clustermountPrefix: gpfs # /gpfs/fs1 mount point on the file system, attribute required for smb exports
filesystemName: fs1 # filesystem, probably easiest to create a filesystem (with quotas enabled) for this script
inodeSpace: root # dependent inode space used, pick a fileset as the parent inode set, the root fileset created for every filesystem is acceptable
# CES fileset attributes, used to define new fileset with quota and samba export (samba export shares name with fileset), these are to be script parameters
filesetName: test6
quotasizeUnit: G
quotahardPercentage: 10
quotasoftSize: 10.5
quotahardSize: "{{ ((quotasoftSize / 100 * quotahardPercentage) + quotasoftSize)|round(1,'ceil')|abs }}" # quota hard limit, acceptable for M G T sizes
# CES Unix directory ownership (might be hardcoded as not useful for samba)
#owner: uizrs
#owner: ui-cloudforms-dev
owner: service_CloudForms
# CES AD users/groups for samba ACL and email notification
#groupmembers: service_CloudForms,service_CloudForms
#groupmembers: ucats,ucasw2,uizrs,bhzajd,ui-cloudforms-dev
groupmembers: ucats
#groupmembers: ui-cloudforms-dev
#groupmembers: ui-cloudforms-dev
#groupmembers: ucats,ucasw2
#groupmembers: ui-thirdpartysupport-essential
# AD winrm connectivity details
ad_host: uiwdcjub04.ad.nottingham.ac.uk # active directory server, when using kerberos (with requisite resolv.conf entry) this must be a fqdn
#ad_host: UIWDCUPK06.nottingham.ac.uk # works local+cf
# loadbalanced kerberos doesnt really work unless there are correct entries and ptr records for the loadbalancer endpoint
#ad_host: cfrm.ad.nottingham.ac.uk
#ad_host: uivlan913vip3.nottingham.ac.uk
# ad_host: 128.243.226.17
# members of cfrm.nottingham.ac.uk
#ad_host: uiwdcdns07.ad.nottingham.ac.uk # works
#ad_host: uiwdcjub04a.ad.nottingham.ac.uk # dns not in kerberos db uiwdcjub04a.ad.nottingham.ac.uk - this maybe arogue PTR record
#ad_host: uiwdcjub04a.ad.nottingham.ac.uk # works
#ad_host: uiwdcupk06.ad.nottingham.ac.uk # works, random timeouts over vpn
#
ad_user: "service_CloudForms" #"service_cloudforms" # AD service account capable of manipulating group membership
ad_pass: "As109pHY4Wi9o7naZnhr#!" # AD service account password
ad_connection: winrm
ad_winrm_transport: kerberos
ad_winrm_kinit_mode: managed # allow ansible to manage own kerberos token, this will use credentials to make token entries required
ad_winrm_message_encryption: auto # can be set to always, depends on ad server profile
ad_port: 5986 # 5985/http for non https transport, UON on-prem use 5986/https
ad_winrm_scheme: https # UON on-prem use 5986/https
ad_winrm_server_cert_validation: ignore
# control email recipients
enable_requester_email: true
enable_customer_email: true
#from_email: placeholder # donotreply@nottingham.ac.uk
from_email: donotreply@nottingham.ac.uk
# smtp server
smtp_relay: smtp.nottingham.ac.uk
smtp_port: 25
# Cloudforms API
api_user: placeholder
api_pass: placeholder

View File

@ -0,0 +1,74 @@
---
# swagger API explorer: https://{{ clusterapiIP }}/ibm/api/explorer/#!/Spectrum_Scale_REST_API_v2/
#
checkfilesetsambaexportEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}"
checkfilesetEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/filesystems/{{ filesystemName }}/filesets/{{ filesetName }}"
createfilesetEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/filesystems/{{ filesystemName }}/filesets"
checkjobEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/jobs/{{ jobId }}"
createfilesetquotaEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/filesystems/{{ filesystemName }}/quotas"
createsambaexportEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares"
#createsambaaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/{{ owner }}"
createsambaaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/{{ ad_object }}"
#checksambaeveryoneaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/\\Everyone" # %5C
#removesambaeveryoneaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/\\Everyone" # %5C
checksambaeveryoneaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/%5CEveryone"
removesambaeveryoneaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl/%5CEveryone"
listsambaaclEndpoint: "https://{{ clusterapiIP }}:443/scalemgmt/v2/smb/shares/{{ filesetName }}/acl"
# createfileset: {
# "filesetName": "{{ filesetName }}",
# "path": "/{{ clustermountPrefix }}/{{ filesystemName }}/{{ filesetName }}",
# "createDirectory": true,
# "owner": "{{ inodeSpace }}",
# "permissions": 755,
# "inodeSpace": "{{ inodeSpace }}"
# }
createfileset: {
"filesetName": "{{ filesetName }}",
"path": "/{{ clustermountPrefix }}/{{ filesystemName }}/{{ filesetName }}",
"createDirectory": true,
"permissions": 755,
"inodeSpace": "{{ inodeSpace }}"
}
quotafileset: {
"operationType": "setQuota",
"quotaType": "FILESET",
"objectName": "{{ filesetName }}",
"blockSoftLimit": "{{ quotasoftSize }}{{ quotasizeUnit }}",
"blockHardLimit": "{{ quotahardSize }}{{ quotasizeUnit }}"
}
smbexportfileset: {
"shareName": "{{ filesetName }}",
"path": "/{{ clustermountPrefix }}/{{ filesystemName }}/{{ filesetName }}",
"smbOptions": {
"browseable": "yes",
"smbEncrypt": "auto",
"comment": "Provisioned by Cloudforms",
"cscPolicy": "manual",
"fileIdAlgorithm": "fsname",
"gpfsLeases": "yes",
"gpfsRecalls": "yes",
"gpfsShareModes": "yes",
"gpfsSyncIo": "no",
"hideUnreadable": "no",
"opLocks": "yes",
"posixLocking": "no",
"readOnly": "no",
"syncOpsOnClose": "no",
"hideDotFiles": "no"
}
}
# following request sets fileset owner with unix permissions I believe
# smbexportfilesetacl: {
# "shareName": "{{ filesetName }}",
# "name": "{{ owner }}",
# "access": "ALLOWED",
# "permissions": "FULL",
# "type": "USER"
# }
smbexportfilesetacl: {
"shareName": "{{ filesetName }}",
"name": "{{ ad_object }}",
"access": "ALLOWED",
"permissions": "FULL",
"type": "USER"
}

View File

@ -0,0 +1,33 @@
## What is this playbook?
It provisions Netapp storage shared over samba.
This playbook was a proof of concept for the native ansible netapp modules, this was written at the start of the self provisioning project.
The methodology of the playbook is not compatible with the working practises of the UoN storage team, it uses the cluster manager ip not the svm ip, it creates a volume not a qtree.
## The playbook demonstrates
Some input validation of parameters.
Create/Delete a flexvol.
Create/Delete a samba share.
Apply ACL to samba share.

View File

@ -0,0 +1,31 @@
# Run playbook with parameters passed from Cloudforms:
#
# Run on command line with parameters where no lists are required:
#
# ansible-playbook vserver.yml -e 'unique_identifier="some_ref" \
# cifs_ad_object="tseed" \
# cifs_administrator_object="administrator" \
# size=2 \
# netapp_hostname="192.168.101.131" \
# netapp_username="admin" \
# netapp_password="Password0" \
# netapp_vserver="netappsim-svm1" \
# perform=create'
#
# Run on command line with parameters as serialised json document where lists are required (cifs_ad_object), size can be omitted from the delete action, dummy ad objects can be used in the delete action
#
# ansible-playbook vserver.yml -e '{"unique_identifier":"some_ref","cifs_ad_object":["tseed","swright"],"cifs_administrator_object":["administrator","ops"],"size":1,"netapp_hostname":"192.168.101.131","netapp_username":"admin","netapp_password":"Password0","netapp_vserver":"netappsim-svm1","perform":"create"}'
# ansible-playbook vserver.yml -e '{"unique_identifier":"some_ref","cifs_ad_object":["tseed"],"cifs_administrator_object":["administrator"],"size":4.1,"netapp_hostname":"192.168.101.131","netapp_username":"admin","netapp_password":"Password0","netapp_vserver":"netappsim-svm1","perform":"create"}'
# ansible-playbook vserver.yml -e '{"unique_identifier":"some_ref","cifs_ad_object":["nobody"],"cifs_administrator_object":["nobody"],"netapp_hostname":"192.168.101.131","netapp_username":"admin","netapp_password":"Password0","netapp_vserver":"netappsim-svm1","perform":"delete"}'
#
---
prefix: "CF"
unique_identifier: "placeholder" #populated by user to uniquely identify share or CF Ref#
cifs_ad_object: "placeholder" #AD username or group
cifs_administrator_object: "placeholder" #AD administrator, likely be an Admin group
size: 1 #size should be integer, will turn float to int
netapp_hostname: "placeholder" #"fqdn or ip"
netapp_username: "placeholder" #"cluster manager account such as admin (not vsadmin)"
netapp_password: "placeholder" #"cluster manager account password"
netapp_vserver: placeholder #svm instance name
perform: "placeholder" #"should be create or delete else should abort"

View File

@ -0,0 +1,157 @@
---
- hosts: localhost
gather_facts: false
name: Create/Delete FlexVol With Cifs Share
vars:
#set present/absent flag for netapp modules from create/delete values in the perform parameter
state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}"
vars_files:
vars/main.yml
tasks:
- name: Convert cif_ad_object to list # use where playbook invoked on command line with single string variables, rather than json input that accepts lists
vars:
cifs_ad_object: ["cifs_ad_object"]
register: cifs_ad_object
when: cifs_ad_object is string
debug: msg="cifs_ad_object {{ cifs_ad_object }} is not a list of users, converting to list"
- name: Convert cifs_administrator_object to list # use where playbook invoked on command line with single string variables, rather than json input that accepts lists
vars:
cifs_administrator_object: ["cifs_administrator_object"]
register: cifs_administrator_object
when: cifs_administrator_object is string
debug: msg="cifs_administrator_object {{ cifs_administrator_object }} is not a list of users, converting to list"
- name: Fail Where Requisite Vars Not Set
fail:
msg: "Parameter {{item.key}} has value {{item.value}}, {{item.key}} is required to be passed from Cloudforms"
when: item.value == 'placeholder'
loop: "{{ lookup('dict', vars ) }}" #vars is a special variable of a list containng all varibales in the playbook
- name: Get Aggregate Available Space
na_ontap_command:
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
command: ['set -showseparator "," -units GB;aggr show -aggregate netapp_sim_01_FC_1 -fields availsize']
register: aggavail
- name: Get Aggregate Size
na_ontap_command:
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
command: ['set -showseparator "," -units GB;aggr show -aggregate netapp_sim_01_FC_1 -fields size']
register: aggtotal
- name: Get Aggregate Used Space
na_ontap_command:
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
command: ['set -showseparator "," -units GB;aggr show -aggregate netapp_sim_01_FC_1 -fields physical-used']
register: aggused
- name: Get Aggregate Space
vars:
#remove newline, remove \", get command line output (was dirty with newlines), get first item of list (output in list format), split result by comma and grab 6th field, remove GB (this might be a different size unit)
availsizeclean: "{{ aggavail.msg | regex_replace('\n','') | regex_replace('\"','') | regex_findall('(?<=<cli-output>)(.*)(?=</cli-output>)') }}"
avail: "{{ availsizeclean[0].split(',')[5] | lower | regex_replace('gb','')}}"
totalclean: "{{ aggtotal.msg | regex_replace('\n','') | regex_replace('\"','') | regex_findall('(?<=<cli-output>)(.*)(?=</cli-output>)') }}"
total: "{{ totalclean[0].split(',')[5] | lower | regex_replace('gb','')}}"
usedclean: "{{ aggused.msg | regex_replace('\n','') | regex_replace('\"','') | regex_findall('(?<=<cli-output>)(.*)(?=</cli-output>)') }}"
used: "{{ usedclean[0].split(',')[5] | lower | regex_replace('gb','')}}"
#aggstats: ["{{ avail }}", "{{ total }}", "{{ used }}"]
#debug: msg="available space {{ avail }}, total space {{ total }}, used space {{ used }}, stats {{ aggstats }}"
set_fact:
aggstats: ["{{ avail }}", "{{ total }}", "{{ used }}"]
- name: Test Available Aggregate Disk Space
# designed to work with thin provisioned aggregates
# should stop creation of volume where aggregate + requested volume size exceed a %utilisation threshold
# should set hard limit for size of volume, as a backstop
# should stop creation of a volume that exceeds the real size of the aggregate (all you see in this scenario is free space remaining on the volume)
vars:
threshold: "{{ aggstats[1] | float / 100 * 70 }}"
toprovision: "{{ aggstats[2] | float + size | float }}"
thicktotal: "{{ aggstats[1] | float }}"
#debug: msg="max disk to use {{ threshold }}GB @ 70% utilisation of total disk {{ thicktotal }}GB, disk requested to provision {{ size }}GB, total disk used if share provisioned {{ toprovision }}GB"
fail:
msg: "provioning new {{ size }}GB volume exceeds 70% threshold capacity of aggregate {{ threshold }}GB, total disk required {{ toprovision }}GB"
when: toprovision | float > threshold | float
- name: Create/Delete FlexVol
na_ontap_volume:
state: "{{ state }}"
#state: absent # now interpolated from the value of perform parameter
name: "{{ prefix }}_{{ unique_identifier }}"
is_infinite: False
aggregate_name: netapp_sim_01_FC_1
# module parameter size only accepts integers, we use float to validate space provisioning so we convert here
size: "{{ size | int }}"
size_unit: gb
junction_path: /{{ prefix }}_{{ unique_identifier }}
volume_security_style: mixed # in use should nfs shares be required, probably not required for just cifs
unix_permissions: 777 # when using mixed security style unix permissions must be set, as this is insecure likely qtrees and allowed hosts would be set for nfs
space_guarantee: none # thin provisioning
#efficiency_policy: # would need to create a policy to include dedupe and compression
vserver: "{{ netapp_vserver }}"
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
- name: Create Cifs Share
na_ontap_cifs:
state: "{{ state }}"
share_name: "{{ prefix }}_{{ unique_identifier }}"
path: /{{ prefix }}_{{ unique_identifier }}
vserver: "{{ netapp_vserver }}"
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
notify:
- Remove Everyone User From Cifs Share
- Add AD user/group To Cifs Share
- Add administrator To Cifs Share
handlers:
- name: Remove Everyone User From Cifs Share
na_ontap_cifs_acl:
state: "absent"
share_name: "{{ prefix }}_{{ unique_identifier }}"
user_or_group: Everyone
vserver: "{{ netapp_vserver }}"
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
- name: Add AD user/group To Cifs Share
when: state == 'present'
na_ontap_cifs_acl:
state: "{{ state }}"
share_name: "{{ prefix }}_{{ unique_identifier }}"
#user_or_group: "{{ cifs_ad_object }}"
user_or_group: "{{ item }}"
permission: full_control
vserver: "{{ netapp_vserver }}"
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
loop: "{{ cifs_ad_object }}"
- name: Add administrator To Cifs Share
when: state == 'present'
na_ontap_cifs_acl:
state: "{{ state }}"
share_name: "{{ prefix }}_{{ unique_identifier }}"
#user_or_group: "{{ cifs_administrator_object }}"
user_or_group: "{{ item }}"
permission: full_control
vserver: "{{ netapp_vserver }}"
hostname: "{{ netapp_hostname }}"
username: "{{ netapp_username }}"
password: "{{ netapp_password }}"
loop: "{{ cifs_administrator_object }}"

View File

@ -0,0 +1,61 @@
## What is this playbook?
It provisions a qtree on an existing volume shared over samba, it will then apply ACL by powershell to replicate the manual steps taken by the UoN storage team.
The playbook was tested on a development build of a netapp single node cluster with an svm connected to a domain.
This playbook is functional but unfinished requiring input validation for cloudforms parameterisation, better error condition reporting and customised UoN branded HTML customer notification email templates.
Time was called when the UoN development netapp svm test account roles were unable to grant sufficient permissions to run this playbook, previously the native ansible modules targeted the netapp cluster manager. This playbook should run against the netapp cluster manager with the included API calls with the exception of the DACL cli API endpoints, these could be changed to run against the cluster manager IP OR the ansible netapp module reinstated, included in the play are the original native ansible module cli commands that would replace the DACL API calls for whomever picks up this task.
The README_DACL.md contains the commands used over ssh to apply the DACL, these could be also be run over ssh by ansible as an alternative to API calls or the ansible netapp cli module.
## The playbook demonstrates
Checks cloudforms environment and changes the name of the service to include the request ID to uniquely identify what has been ordered.
Check the AD users/groups provided for the share permissions are valid.
Builds a list of users/groups who will have access to the samba share.
Builds a list of users is nested groups and looks up from AD their associated email address to be used in access notification emails.
Creates a qtree on the target volume.
Creates a quota for the qtree.
Toggles volume quotas off then on to ensure the qtree level quota takes effect.
Creates DACL policy for a service account that will later change folder ACL over the samba share.
Runs powershell via a windows host to change the folder ACL presented over samba.

View File

@ -0,0 +1,48 @@
## DACL cli commands to give a user full permission to the qtree folder
- These commands are run from the cluster controller via ssh.
- To run these commands on the SVM remove the term -vserver netappsim-svm1.
#### create a policy
vserver security file-directory policy create -vserver netappsim-svm1 -policy-name myqtree
#### create and add rules to a security descriptor
vserver security file-directory ntfs dacl add -vserver netappsim-svm1 -ntfs-sd myqtree -access-type allow -account NETAPPSIM\administrator -rights full-control -apply-to this-folder,sub-folders,files
#### create a task that adds security descriptor to the policy at a given path
vserver security file-directory policy task add -vserver netappsim-svm1 -policy-name myqtree -path /k_t3fp_b_cifs_r15/myqtree -ntfs-sd myqtree -ntfs-mode propagate -security-type ntfs
#### apply the policy
vserver security file-directory apply -vserver netappsim-svm1 -policy-name myqtree
#### delete the policy
vserver security file-directory policy delete myqtree
- It is safe to delete the policy, this will not effect the ACL's you
have just applied to the qtree.
#### delete security descriptor rules
vserver security file-directory ntfs dacl remove -ntfs-sd myqtree -access-type *
- There is no need to clear the security descriptor rule when deleting
the security descriptor.
#### delete security descriptor
vserver security file-directory ntfs delete -ntfs-sd myqtree
- It is safe to delete the security descriptor, this will not effect
the ACL's you have just applied to the qtree.
#### check for effective permissions and leftover policy / security descriptor
vserver security file-directory show -vserver netappsim-svm1 -path /k_t3fp_b_cifs_r15/myqtree
vserver security file-directory ntfs show
vserver security file-directory policy show

View File

@ -0,0 +1,191 @@
---
- name: nested groups loop
block:
- name: Reset loop variables # without unsetting infinte loops will occur
set_fact:
group_present: false
find_groups: []
#find_users: [] # we dont want to reset this var, this will append on each loop until exit (unexpected behaviour but works)
find_group_members_tidy: []
group_members: []
samaccountname_tidy: []
samaccountname_group_members: []
member_type: []
member_attributes: []
# - name: Inspect dummy host object_attributes
# debug:
# msg: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
- name: Filter groups into a list - CHANGED
set_fact:
find_groups: "{{ find_groups | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
with_items: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
when: item.type == 'Group'
- name: Append users to list - CHANGED
set_fact:
find_users: "{{ find_users | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
with_items: "{{ hostvars['DUMMY_HOST']['object_attributes'] }}"
when: item.type == 'Person'
# - debug:
# msg:
# - "{{ find_groups }}"
# - "{{ find_users }}"
# when: find_groups is defined
- name: Query group members
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.member
register: find_group_members_result
with_items: "{{ find_groups }}"
when: find_groups is defined
# - debug:
# msg: "{{ find_group_members_result }}"
# when: find_group_members_result is defined
- name: Tidy group members into dict of names and role
set_fact:
find_group_members_tidy: '{{ find_group_members_tidy | default([]) + [dict(name=item.name, role=item.role)] }}'
with_items: "{{ find_group_members_result | json_query(jmesquery) }}"
vars:
jmesquery: "results[].{role: @.item.role, name: @.stdout_lines}"
when: find_groups is defined # new - did i miss this in the demo
- name: Tidy group members into dict of name and role
set_fact:
group_members: "{{ group_members | default([]) + [dict(name=item.1.split(',')[0].split('CN=')[1], role=item.0.role)] }}"
with_subelements:
- "{{ find_group_members_tidy }}"
- name
when: find_groups is defined # new - did i miss this in the demo
# - debug:
# msg: "{{ group_members }}"
# when: group_members is defined
# the UoN account field distinguishedname uses the short account name the same as samaccountname - this is helpful when looking up group members
# distinguishedname {CN=tseed,CN=Users,DC=netappsim,DC=local}
# samaccountname {tseed}
#
# out of the box AD will have the full name of the user in the distinguishedname field
# distinguishedname {CN=Toby Seed,CN=Users,DC=netappsim,DC=local}
# samaccountname {tseed}
#
# an additional check follows to lookup a samaccountname using the distinguishedname, this maintains compatibility with non UoN AD
- name: Find group members sAMAccountName
win_shell: ([ADSISearcher] "(cn={{ item.name }})").FindOne().Properties.samaccountname
register: samaccountname_result
with_items: "{{ group_members }}"
when: group_members is defined
- name: Tidy group members sAMAccountName into list
set_fact:
samaccountname_tidy: "{{ samaccountname_tidy | default([]) + [(item.stdout_lines)[0]] }}"
with_items: "{{ samaccountname_result.results }}"
when: group_members is defined
- name: Rebuild group_members dict with sAMAccountName
set_fact:
samaccountname_group_members: "{{ samaccountname_group_members | default([]) + [ dict(name=item[1], role=item[0].role) ] }}"
loop: "{{ group_members|zip(samaccountname_tidy)|list }}"
when: group_members is defined
# - debug:
# msg: "{{ samaccountname_group_members }}"
# when: group_members is defined
- name: copy samaccountname_group_members to group_members
set_fact:
group_members: "{{ samaccountname_group_members }}"
when: samaccountname_group_members is defined
# - debug:
# msg: "{{ group_members }}"
# when: group_members is defined
- name: Check AD object is user or group
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.name }})").FindOne().Properties.objectcategory
register: member_type_result
with_items: "{{ group_members }}"
when: group_members is defined
# - debug:
# msg: "{{ member_type_result }}"
# when: group_members is defined
- name: Build list of AD object type
set_fact:
member_type: "{{ member_type | default([]) + [(item.stdout.split(',')[0].split('CN=')[1])] }}"
with_items: "{{ member_type_result.results }}"
when: group_members is defined
# - debug:
# msg: "{{ member_type }}"
# when: group_members is defined
- name: Build dict of object names and types
set_fact:
member_attributes: "{{ member_attributes | default([]) + [ dict(name=item[0].name, role=item[0].role, type=item[1]) ] }}" # effectively adding a new positional field from the list to the dict
loop: "{{ group_members|zip(member_type)|list }}"
when: group_members is defined
# - debug:
# msg: "{{ member_attributes }}"
# when: group_members is defined
- name: Reset/Add variable object_attributes to dummy host for next loop
add_host:
name: "DUMMY_HOST"
object_attributes: "{{ member_attributes }}"
when: member_attributes is defined
- name: Set flag to notify there are nested groups
set_fact:
group_present: true
with_items: "{{ member_attributes }}"
when: member_attributes is defined and item.type == 'Group'
#when: member_attributes is defined and item.type == 'GroupA' # used to break loop with test for no group_present
- name: Nested group present?
fail:
msg: Nested group detected, loop will run again
when: group_present
# the following tasks only run on the last run of the loop where there are no more groups detected
# append the users from the last run of the loop to find_users
# set find_users as a DUMMY_HOST variable to be retrieved from the calling script - this is how you pass variables back from different plays also works for include_tasks
- name: Append users to list
set_fact:
find_users: "{{ find_users | default([]) + [dict(name=item.name, type=item.type, role=item.role)] }}"
with_items: "{{ member_attributes }}"
when: item.type == 'Person'
# if the script was passed the members parameter containing only a group and the requester_user_ad is a member of this group
# there wont be an entry in the find_users dict as this wont have been caught and assigned the role='requestor' in the calling script
# all requester flag logic could be moved here, the calling script wouldnt need to pass the role key, we keep it for illustration as
# this script was origionally designed to assign roles for different RWX permissions on GPFS
# we add the user to the dict with role='requester'
- name: Check if requestor was nested in group and not passed in the members parameter
set_fact:
requester_found: "{{ item.name }}"
with_items: "{{ find_users }}"
when: item.name == requester_user_ad
- name: Add requestor if it was nested in group and not passed in the members parameter
set_fact:
find_users: "{{ find_users + [dict(name=requester_found, type='Person', role='requester')] }}"
when: requester_found is defined
- name: Add users to variable find_users for dummy host on final loop
add_host:
name: "DUMMY_HOST"
find_users: "{{ find_users }}"
when: find_users is defined
rescue:
- include_tasks: group_lookup.yml

View File

@ -0,0 +1,782 @@
---
# PLAY
# Gather cloudforms information and set service name
- name: Query automate workspace
hosts: localhost
gather_facts: False
vars_files:
vars/main.yml
tasks:
- set_fact:
endpoint: "{{ manageiq.api_url }}"
auth_token: "{{ manageiq.api_token }}"
request_id: "{{ (manageiq.request).split('/')[-1] }}"
service_id: "{{ (manageiq.service).split('/')[-1] }}"
user_id: "{{ (manageiq.user).split('/')[-1] }}"
when: manageiq is defined
# when users run ansible their manageiq auth token does not have sufficient rights to interact with the API, no combination of rights in a role for a non admin user are sufficient
# use the local admin credentials to get an auth token
- name: Get auth token
uri:
# ansible runner timeout, something changed from 5.11.1.2 to 5.11.6.0?
#url: "{{ endpoint }}/api/auth"
url: "https://127.0.0.1/api/auth"
validate_certs: no
method: GET
user: "{{ api_user }}"
password: "{{ api_pass }}"
status_code: 200
register: login
when: manageiq is defined
- set_fact:
auth_token: "{{ login.json.auth_token }}"
when: manageiq is defined
- name: Get requester user attributes
uri:
#url: "{{ endpoint }}/api/users/{{ user_id }}"
url: "https://127.0.0.1/api/users/{{ user_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: user
when: manageiq is defined
- set_fact:
requester_email: "{{ user.json.email }}"
when: manageiq is defined and user.json.email is not none # cloudforms admin user has no email address (unless set), this is a null field in json
- set_fact:
requester_email: "{{ from_email }}" # from_email should be able to recieve mail via relay
when: requester_email is not defined
- set_fact:
requester_user: "{{ user.json.name }}"
when: manageiq is defined
# when run from the command line set a default requester user
- set_fact:
requester_user: "command line invocation"
when: manageiq is not defined
- name: define qtree name suffix, use the CloudForms request id as the qtree name suffix
set_fact:
qtree_suffix: "{{ request_id }}"
when: manageiq is defined
- name: generate qtree name suffix, generate random string for qtree name suffix
shell: head /dev/urandom | tr -dc A-Z0-9 | head -c 10 ; echo ''
register: qtree_suffix
when: manageiq is not defined
- name: define qtree name suffix
set_fact:
qtree_suffix: "{{ qtree_suffix.stdout }}"
when: manageiq is not defined
# - name: DEBUG print all user attributes
# debug:
# msg:
# - "{{ user }}"
#
# useful fields:
# "email": "ucats@exmail.nottingham.ac.uk"
# "name": "Toby Seed"
# "userid": "toby.seed@nottingham.ac.uk"
#
# we see that the UON active directory schema has a different correlation between the AD short login id and the lookup of the userid and name
# the above user generally would use the login id "ucats" across the UON estate to authenticate against AD
# interestingly cloudforms queries several fields in the schema and will allow login as "ucats" and "toby.seed@nottingham.ac.uk"
# to detect if this script is being run in self service mode, a match is performed against the requesting user and a single entry in groupmember list
# we can only retrieve the expected AD short login id from the email field with what is passed by cloudforms when using UON AD
- name: get AD account name
set_fact:
requester_user_ad: "{{ (requester_email).split('@')[0] }}"
#when: manageiq is defined
- name: get service
uri:
url: "https://127.0.0.1/api/services/{{ service_id }}"
validate_certs: no
method: GET
headers:
X-Auth-Token: "{{ auth_token }}"
status_code: 200
register: service
when: manageiq is defined
- set_fact:
service_name: "{{ service.json.name }}"
new_service_name: "{{ service.json.name }} {{ request_id }}"
when: manageiq is defined
# not using yet - will be used in emails
# - set_fact:
# service_name: "command line invocation"
# new_service_name: "command line invocation"
# when: manageiq is not defined
- name: set service name
uri:
url: "https://127.0.0.1/api/services/{{ service_id }}"
validate_certs: no
method: POST
headers:
X-Auth-Token: "{{ auth_token }}"
body_format: json
body: { "action" : "edit", "resource" : { "name" : "{{ new_service_name }}" }}
status_code: 200, 204
register: service
when: manageiq is defined
# PLAY
# Validate parameters passed to script from cloudforms
- name: Script input Validation
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
vars:
groupmemberslist: []
groupmemberslistvalidate: []
tasks:
# REQUIREMENT
# need a task to check for placeholder values here when parameterized for cloudforms
- name: Split groupmembers parameter on , delimiter
set_fact:
groupmemberslist: "{{ groupmemberslist }} + [ '{{ item }}' ]"
with_items: "{{ members.split(',') }}"
- name: Remove empty fields from groupmembers parameter
set_fact:
groupmemberslistvalidate: "{{ groupmemberslistvalidate }} + [ '{{ item }}' ]"
when: item | length != 0
with_items: "{{ groupmemberslist }}"
- name: Remove duplicate entries
set_fact:
groupmemberslistvalidate: "{{ groupmemberslistvalidate | unique }}"
# PLAY
# Add winrm host used for AD querys to inventory
- name: Build inventory for AD server
hosts: localhost
gather_facts: False
vars_files:
- vars/main.yml
tasks:
- name: Add host entry for adserver
add_host: >
name=adserver
groups=windows
ansible_host="{{ ad_host }}"
# PLAY
# Query winrm to validate AD user/group exist, build dict for share ACL and dict of all user/email from a recursively search of user/group
- name: Check AD user exists
hosts: adserver
gather_facts: false
vars_files:
- vars/main.yml
vars:
groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}"
requester_user_ad: "{{ hostvars['localhost']['requester_user_ad'] }}"
no_aduser: []
object_type: []
tasks:
- name: Add connectivity variables for adserver
set_fact:
ansible_user: "{{ ad_user }}"
ansible_password: "{{ ad_pass }}"
ansible_connection: "{{ ad_connection }}"
ansible_winrm_transport: "{{ ad_winrm_transport }}"
ansible_winrm_kinit_mode: "{{ ad_winrm_kinit_mode }}"
ansible_winrm_message_encryption: "{{ ad_winrm_message_encryption }}"
ansible_port: "{{ ad_port }}"
ansible_winrm_scheme: "{{ ad_winrm_scheme }}"
ansible_winrm_server_cert_validation: "{{ ad_winrm_server_cert_validation }}"
# ansible_winrm_operation_timeout_sec: 60
# ansible_winrm_read_timeout_sec: 60
- name: Check AD user/group exists
win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne()
register: command_result
with_items:
- "{{ groupmemberslistvalidate }}"
- name: Flag fail where AD user/group not exist
set_fact:
no_aduser: "{{ no_aduser + [item.item] }}"
when: item.stdout | length == 0
with_items: "{{ command_result.results }}"
changed_when: true
#notify: topic_noad # would be used for handler for failure conditions by console and email
- name: Check AD object is user or group
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.item }})").FindOne().Properties.objectcategory
register: object_result
with_items: "{{ command_result.results }}"
when: no_aduser | length == 0
- name: Build list of AD object type
set_fact:
object_type: "{{ object_type + [(item.stdout.split(',')[0].split('CN=')[1])] }}" # faster than regex
with_items: "{{ object_result.results }}"
when: no_aduser | length == 0
# if the cloudforms requester's AD account requester_user_ad is in the list of users, set the role requester, this will be used for self service emails
- name: Build dict of object names, types and roles positionally from list with object name and list with object type
set_fact:
object_attributes: "{{ object_attributes | default([]) + [dict(name=item[0], type=item[1], role='requester' if (item[0] == requester_user_ad) else 'member') ] }}"
loop: "{{ groupmemberslistvalidate|zip(object_type)|list }}"
when: no_aduser | length == 0
- name: Register dummy host with variable object_attributes
add_host:
name: "DUMMY_HOST"
object_attributes: "{{ object_attributes }}"
when: no_aduser | length == 0
- name: Find all group members
include_tasks: group_lookup.yml
when: no_aduser | length == 0
# - name: Inspect all users who will require email notification
# debug:
# msg: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
# when: no_aduser | length == 0
- name: Import dummy host variable from group_lookup.yml
set_fact:
unique_users: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
when: no_aduser | length == 0
- name: Get unique name/role/type entries, remove duplicate entries that may arise from group nesting
set_fact:
unique_users: "{{ unique_users | unique }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ unique_users }}"
# when: no_aduser | length == 0
- name: Get requester name into a list
set_fact:
requester_users: "{{ requester_users | default([]) + [item.name] }}"
with_items: "{{ unique_users }}"
when: no_aduser | length == 0 and item.role == 'requester'
- name: Get member names into a list
set_fact:
member_users: "{{ member_users | default([]) + [item.name] }}"
with_items: "{{ unique_users }}"
when: no_aduser | length == 0 and item.role == 'member'
- name: Get names common in both lists
set_fact:
common_users: "{{ requester_users | intersect(member_users) }}"
when: no_aduser | length == 0 and requester_users is defined
- name: Remove member entries where competing requester entry exists
set_fact:
email_users: "{{ email_users | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
with_items: "{{ unique_users }}"
#when: no_aduser | length == 0 and (item.name not in common_users or (item.name in common_users and item.role == 'requester'))
when: no_aduser | length == 0 and (requester_users is defined and (item.name not in common_users or (item.name in common_users and item.role == 'requester')))
- name: Set email_users where requester not present in the user list
set_fact:
email_users: "{{ unique_users }}"
when: no_aduser | length == 0 and requester_users is not defined
# deduplicated dict to send appropriate class of email to users
# - debug:
# msg: "{{ email_users }}"
# when: no_aduser | length == 0
- name: Get member email address from AD
win_shell: Get-ADUser {{ item.name }} -Properties mail | Select-Object -ExpandProperty mail
register: email_result
with_items: "{{ email_users }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ email_result }}"
# when: no_aduser | length == 0
# this would crash out where customer account has no associated email, UoN are very consistent with account creation and adding email
# check for an empty email and substitute for 'none', this dict key will be evaluated when sending emails
- name: Get member emails into a list
set_fact:
email_address: "{{ email_address | default([]) + [item.stdout_lines[0] if (item.stdout_lines | length > 0) else 'none' ] }}"
with_items: "{{ email_result.results }}"
when: no_aduser | length == 0
# - debug:
# msg: "{{ email_address }}"
- name: Build dict of object name, role, type and email
set_fact:
user_dict: "{{ user_dict | default([]) + [dict(name=item[0].name, role=item[0].role, type=item[0].type, email=item[1])] }}" # adding a new positional field from the list to the dict
loop: "{{ email_users|zip(email_address)|list }}"
when: no_aduser | length == 0
- debug:
msg:
- "{{ object_attributes }}" # use for the ACL's
- "{{ user_dict }}" # use to identify users that will get various classes of email
when: no_aduser | length == 0
# REQUIREMENT
# drop in handlers for noad failure here
# PLAY
# Create qtree / quota / dacl and define windows host
- hosts: localhost
gather_facts: false
name: Create/Delete qtree and quota
vars:
#state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}" # no remove logic yet
qtree_suffix: "{{ hostvars['localhost']['qtree_suffix'] }}"
ADPSuser: "{{ domain }}\\{{ ad_user }}"
vars_files:
- vars/main.yml
- vars/requests.yml
tasks:
- set_fact:
human_to_byte_string: "{{ qtree_quota }} {{ qtree_quota_unit }}"
- set_fact:
quota_hard_limit: "{{ human_to_byte_string|human_to_bytes}}"
quota_soft_limit: "{{( human_to_byte_string|human_to_bytes | float / 100 * qtree_quota_soft_limit) | round | int | abs }}"
# when run from cloudforms we would pass the prefix/suffix from the dialog or set from the retrieved request ID
- name: generate qtree name suffix, generate random string for qtree name suffix
shell: head /dev/urandom | tr -dc A-Z0-9 | head -c 10 ; echo ''
register: qtree_suffix
- name: define qtree name suffix
set_fact:
qtree_suffix: "{{ qtree_suffix.stdout }}"
- set_fact:
qtree_name: "{{ qtree_prefix }}_{{ qtree_suffix }}"
# REQUIREMENT
# volume space reporting, actual space vs over provisioned space + accociated failure consition and console/email reporting via handler
# REQUIREMENT
# check qtree name doesnt already exist and hard exit (for cloudforms failed request) with a requester email
# REQUIREMENT
# all API requests need their own include tasks and status_code evaluations with handler topics
# far too much repetition of queued job checking
- name: Get svm UUID
uri:
url: "{{ getSvmEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
status_code: 200
register: response
- set_fact:
svm_uuid: "{{ (response.json.records | json_query(jmesquery))[0] }}"
vars:
jmesquery: "[?name == '{{ netapp_svm_name }}'].uuid"
- name: Get volume UUID
uri:
url: "{{ getVolumesEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
status_code: 200
register: response
- set_fact:
volume_uuid: "{{ (response.json.records | json_query(jmesquery))[0] }}"
vars:
jmesquery: "[?name == '{{ volume_name }}'].uuid"
- name: Create qtree
uri:
url: "{{ postQtreeEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postQtree }}"
validate_certs: no
return_content: yes
status_code: 202
register: response
- set_fact:
job_uuid: "{{ response.json.job.uuid }}"
- name: Get job status
uri:
url: "{{ getJobEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
register: check_response
until: check_response.json.state != 'running'
retries: "{{ api_retry }}"
delay: 10
- name: Report job failure
fail:
msg:
- "{{ check_response.json.message }}"
when: check_response.json.state == 'failure'
# not required
# - name: Get qtree ID
# uri:
# url: "{{ getQtreeEndpoint }}"
# user: "{{ netapp_svm_user }}"
# password: "{{ netapp_svm_pass }}"
# method: GET
# validate_certs: no
# return_content: yes
# status_code: 200
# register: response
# - set_fact:
# qtree_id: "{{ (response.json.records | json_query(jmesquery))[0] }}"
# vars:
# jmesquery: "[?name == '{{ qtree_name }}'].id"
- name: Create quota
uri:
url: "{{ postQuotaEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postQuota }}"
validate_certs: no
return_content: yes
status_code: 202
register: response
- set_fact:
job_uuid: "{{ response.json.job.uuid }}"
- name: Get job status
uri:
url: "{{ getJobEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
register: check_response
until: check_response.json.state != 'running'
retries: "{{ api_retry }}"
delay: 10
- name: Report job failure
fail:
msg:
- "{{ check_response.json.message }}"
when: check_response.json.state == 'failure'
- set_fact:
toggle_quota: "false"
- name: Toggle volume quota off
uri:
url: "{{ toggleVolQuotaEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: PATCH
body_format: json
body: "{{ toggleVolQuota }}"
validate_certs: no
return_content: yes
status_code: 202
register: response
- set_fact:
job_uuid: "{{ response.json.job.uuid }}"
- name: Get job status
uri:
url: "{{ getJobEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
register: response
until: response.json.state != 'running'
retries: "{{ api_retry }}"
delay: 10
- name: Report job failure
fail:
msg:
- "{{ response.json.message }}"
when: response.json.state == 'failure'
# the API will report a quota job finished but blocks another quota command, requires a brief pause on a busy system
- pause:
seconds: "{{ netapp_cli_sleep }}"
- set_fact:
toggle_quota: "true"
- name: Toggle volume quota on
uri:
url: "{{ toggleVolQuotaEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: PATCH
body_format: json
body: "{{ toggleVolQuota }}"
validate_certs: no
return_content: yes
status_code: 202
register: response
- set_fact:
job_uuid: "{{ response.json.job.uuid }}"
# enabling quota will take some time, ensure the retries cover this with larger filesystems
- name: Get job status
uri:
url: "{{ getJobEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
register: response
until: response.json.state != 'running'
retries: "{{ api_retry }}"
delay: 10
- name: Report job failure
fail:
msg:
- "{{ response.json.message }}"
when: response.json.state == 'failure'
# these commented tasks are included to show the ansible native ontap cli module, the play moved to using the SVM instead of the ClusterManager on request of the storage team, necessitating API calls rather than the ontap modules
# if the ClusterManager target is reinstated for this play, either modify the CLI API endpoints (preferable due to job control) or use this module and be mindful of sleep command
#
# there is no simple one liner for the cli to wait for the job to finish, there is a sleep command to mitigate but may need to be tuned on a busy system
# - name: Apply DACL for winrm powershell user account
# na_ontap_command:
# command:
# - 'vserver security file-directory policy create -vserver {{ netapp_vserver_name }} -policy-name {{ qtree_name }};'
# - 'vserver security file-directory ntfs dacl add -vserver {{ netapp_vserver_name }} -ntfs-sd {{ qtree_name }} -access-type allow -account {{ ADPSuser }} -rights full-control -apply-to this-folder,sub-folders,files;'
# - 'vserver security file-directory policy task add -vserver {{ netapp_vserver_name }} -policy-name {{ qtree_name }} -path /{{ volume_name }}/{{ qtree_name }} -ntfs-sd {{ qtree_name }} -ntfs-mode propagate -security-type ntfs;'
# - 'vserver security file-directory apply -vserver {{ netapp_vserver_name }} -policy-name {{ qtree_name }};'
# - 'echo about to remove policy {{ qtree_name }} and security descriptor {{ qtree_name }};'
# - 'sleep {{ netapp_cli_sleep }};'
# - 'vserver security file-directory policy delete {{ qtree_name }};'
# - 'vserver security file-directory ntfs delete -ntfs-sd {{ qtree_name }}'
# privilege: 'admin'
# return_dict: false # fails with compound commands when true
# https: true
# validate_certs: false
# use_rest: Always
# hostname: "{{ netapp_hostname }}"
# username: "{{ netapp_username }}"
# password: "{{ netapp_password }}"
# ignore_errors: True
# register: ontapCmd
# - debug:
# msg:
# - "DACL application failed, try increasing the timeout in the command list"
# - "{{ ontapCmd.msg }}"
# when: ontapCmd.failed
- name: Create DACL policy
uri:
url: "{{ postDACLPolicyEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postDACLPolicy }}"
validate_certs: no
return_content: yes
status_code: 201
register: response
- name: Create DACL policy attributes
uri:
url: "{{ postDACLPolicyAttributesEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postDACLPolicyAttributes }}"
validate_certs: no
return_content: yes
status_code: 200
register: response
- name: Create DACL policy target
uri:
url: "{{ postDACLPolicyTargetEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postDACLPolicyTarget }}"
validate_certs: no
return_content: yes
status_code: 200
register: response
- name: Apply DACL policy
uri:
url: "{{ postDACLPolicyApplyEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: POST
body_format: json
body: "{{ postDACLPolicyApply }}"
validate_certs: no
return_content: yes
status_code: 200
register: response
- set_fact:
job_uuid: "{{ response.json.job.uuid }}"
- name: Get job status
uri:
url: "{{ getJobEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: GET
validate_certs: no
return_content: yes
register: check_response
until: check_response.json.state != 'running'
retries: "{{ api_retry }}"
delay: 10
- name: Report job failure
fail:
msg:
- "{{ check_response.json.message }}"
when: check_response.json.state == 'failure'
- name: Delete DACL policy
uri:
url: "{{ deleteDACLPolicyEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: DELETE
validate_certs: no
return_content: yes
status_code: 200
register: response
- name: Delete DACL security descriptor
uri:
url: "{{ deleteDACLPolicyAttributesEndpoint }}"
user: "{{ netapp_svm_user }}"
password: "{{ netapp_svm_pass }}"
method: DELETE
validate_certs: no
return_content: yes
status_code: 200
register: response
# REQUIREMENT
# handlers needed for this play, console and email failure conditions
# PLAY
# Set windows host connection parameters and run powershell over winrm
- hosts: adserver
gather_facts: false
become_method: runas
name: Change windows ACL for qtree
vars:
qtree_name: "{{ hostvars['localhost']['qtree_name'] }}"
object_attributes: "{{ hostvars['localhost']['object_attributes'] }}"
vars_files:
vars/main.yml
tasks:
# to avoid using group_vars we set_facts that were not accepted by add_host, add_host does not work with many windows winrm connectivity vars
- name: Add connectivity variables for adserver
set_fact:
ansible_user: "{{ ad_user }}"
ansible_password: "{{ ad_pass }}"
ansible_connection: "{{ ad_connection }}"
ansible_winrm_transport: "{{ ad_winrm_transport }}"
ansible_winrm_kinit_mode: "{{ ad_winrm_kinit_mode }}"
ansible_winrm_message_encryption: "{{ ad_winrm_message_encryption }}"
ansible_port: "{{ ad_port }}"
ansible_winrm_scheme: "{{ ad_winrm_scheme }}"
ansible_winrm_server_cert_validation: "{{ ad_winrm_server_cert_validation }}"
#ansible_winrm_operation_timeout_sec: 60
#ansible_winrm_read_timeout_sec: 60
- name: Copy powershell script to winrm host
win_template:
src: templates/ps_acl.ps1.j2
dest: "{{ temp_dir }}{{ qtree_name }}.ps1"
# remove everyone permission set users/group permission, cannot be achieved with DACL
- name: Apply ACL to share
#win_command: powershell.exe -ExecutionPolicy Unrestricted {{ qtree_name }}.ps1 # powershell permission model is awkward over win_shell and win_command
win_command: powershell.exe -ExecutionPolicy ByPass -File {{ temp_dir }}{{ qtree_name }}.ps1
become: yes
become_user: Administrator # service_cloudforms may need local admin permissions or some winrm permission elevation in a prod environment
register: command_result
- debug:
msg: "{{ command_result }}"
- name: Remove powershell script from winrm host
win_file:
path: "{{ temp_dir }}{{ qtree_name }}.ps1"
state: absent
- debug:
msg:
- "//{{netapp_svm_host}}/{{volume_name}}/{{ qtree_name }}"
# REQUIREMENT
# new play for reporting
# handlers that evaluate state vars needed for this play, console and email failure conditions and (templated) provisioned emails to all users
# will need logic for self service mode from the requester key in the appropriate dict
# REQUIREMENT?
# there is no logic for delete/un-provision, playbook needs breaking out to include tasks that are conditionally run based on perform parameter

View File

@ -0,0 +1,22 @@
#define share
$share = "\\{{ netapp_svm_host }}\{{ volume_name }}\{{ qtree_name }}"
#block inheritance from parent netapp volume and remove inherited permissions
$acl = Get-Acl $share
$acl.SetAccessRuleProtection($true,$false)
$acl | Set-Acl $share
#set ownership for netapp account BUILTIN\Administrators
$acl = Get-Acl $share
$object = New-Object System.Security.Principal.Ntaccount("BUILTIN\Administrators")
$acl.SetOwner($object)
$acl | Set-Acl $share
#set permissions for members with inheritance for subfolders and files
{% for item in object_attributes %}
$acl = Get-Acl $share
$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("{{ domain }}\{{ item.name }}","ExecuteFile, ReadData,ReadAttributes,ReadExtendedAttributes,Createfiles,AppendData,WriteAttributes,WriteExtendedAttributes,DeleteSubdirectoriesAndFiles,Delete,ReadPermissions","ContainerInherit, ObjectInherit","InheritOnly","Allow")
$acl.SetAccessRule($AccessRule)
$acl | Set-Acl $share
{% endfor %}

View File

@ -0,0 +1,62 @@
---
# mandatory parameters for actions
perform: create # should be "create" or "delete" not yet used in playbook
members: tseed,swright,project,architect # users or groups to be applied to share ACL, this is a comma separated list
#members: ,,OCF-ADM,OCF-ADM,tseed,,,,tseed # used to check input validation, dedupe and requester logic
#members: ucats,ucasw2,uizrs,bhzajd,ui-cloudforms-dev # used to check UoN AD and nested-nested-nestedN group lookup
# email parameters
smtp_relay: smtp.nottingham.ac.uk
smtp_port: 25
from_email: ucats@exmail.nottingham.ac.uk # should be a service account address such as donotreply@nottingham.ac.uk
# cloudforms API
api_user: placeholder
api_pass: placeholder
# windows host parameters that run powershell against qtree to set ACL
#
# variables to create in-memory inventory of the AD server, notice the ad_ variables are in the winrm format that would be under the entry [<hostgroup>:vars] for an inventory file
#
ad_host: WIN-1JE0R5GCBSG.NETAPPSIM.LOCAL # active directory server, this must be a fqdn (system-wide kerberos must be working with requisite krb5.conf + resolv.conf entries/tickets to find ad.nottingham.ac.uk)
ad_user: administrator # AD service account capable of manipulating group membership and run powershell ACL against share
ad_pass: "Password0" # AD service account password
domain: NETAPPSIM # domain used in DACL
ad_connection: winrm
ad_winrm_transport: kerberos
ad_winrm_kinit_mode: managed # allow ansible to manage own kerberos token, SSSD manages when set to manual
ad_winrm_message_encryption: auto # can be set to always, depends on ad server profile
ad_port: 5986 # 5985/http for non https transport, UON on-prem use 5986/https
ad_winrm_scheme: https # UON on-prem use 5986/https
ad_winrm_server_cert_validation: ignore
temp_dir: C:\Windows\Temp\ # temporary location for powershell script
#
#ad_host: uiwdcjub04.ad.nottingham.ac.uk
#ad_user: "service_CloudForms"
#ad_pass: "As109pHY4Wi9o7naZnhr#!"
#domain: AD
#
# netapp svm connection parameters
netapp_svm_host: 192.168.101.132 #fqdn or ip, UoN test svm not in dns netappsim-svm1
netapp_svm_name: netappsim-svm1 #svm instance name, used in API
netapp_svm_user: vsadmin #svm user
netapp_svm_pass: Password0 #svm password
volume_name: "k_t3fp_b_cifs_r15" #volume where qtree is to be created
#
# netapp_svm_host: 10.159.144.130
# netapp_svm_name: UIDFSNET01_SVM999
# netapp_svm_user: "ad\service_CloudForms"
# netapp_svm_pass: "As109pHY4Wi9o7naZnhr#!"
# volume_name: "TESTFlexgroup"
#
# qtree and quota settings
qtree_prefix: "CF" # qtree prefix, CF_12345
qtree_quota: 2 # size
qtree_quota_unit: GB # MB, GB, TB, PB
qtree_quota_soft_limit: 70 # % of qtree_quota
# tuning API calls
api_retry: 10 # number of retries
netapp_cli_sleep: 5 # seconds wait

View File

@ -0,0 +1,67 @@
# https://<cluster_mgmt_ip_address>/docs/api
getSvmEndpoint: "https://{{ netapp_svm_host }}/api/svm/svms"
getVolumesEndpoint: "https://{{ netapp_svm_host }}/api/storage/volumes"
postQtreeEndpoint: "https://{{ netapp_svm_host }}/api/storage/qtrees"
getJobEndpoint: "https://{{ netapp_svm_host }}/api/cluster/jobs/{{ job_uuid }}"
getQtreeEndpoint: "https://{{ netapp_svm_host }}/api/storage/qtrees"
postQuotaEndpoint: "https://{{ netapp_svm_host }}/api/storage/quota/rules"
toggleVolQuotaEndpoint: "https://{{ netapp_svm_host }}/api/storage/volumes/{{ volume_uuid }}"
postDACLPolicyEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/policy"
postDACLPolicyAttributesEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/ntfs/dacl/add"
postDACLPolicyTargetEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/policy/task/add"
postDACLPolicyApplyEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/apply"
deleteDACLPolicyEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/policy?policy_name={{ qtree_name }}"
deleteDACLPolicyAttributesEndpoint: "https://{{ netapp_svm_host }}/api/private/cli/vserver/security/file-directory/ntfs?ntfs_sd={{ qtree_name }}"
postQtree: {
"name": "{{ qtree_name }}",
"security_style": "ntfs",
"svm": {
"name": "{{ netapp_svm_name }}",
"uuid": "{{ svm_uuid }}"
},
"volume": {
"name": "{{ volume_name }}",
"uuid": "{{ volume_uuid }}"
}
}
toggleVolQuota: {
"quota": {
"enabled": "{{ toggle_quota }}"
}
}
postQuota: {
"qtree": {
"name": "{{ qtree_name }}"
},
"space": {
"hard_limit": "{{ quota_hard_limit}}",
"soft_limit": "{{ quota_soft_limit }}"
},
"svm": {
"name": "{{ netapp_svm_name }}"
},
"type": "tree",
"volume": {
"name": "{{ volume_name }}"
}
}
postDACLPolicy: {
"policy-name" : "{{ qtree_name }}"
}
postDACLPolicyAttributes: {
"ntfs-sd": "{{ qtree_name }}",
"access-type": "allow",
"account": "{{ ADPSuser }}",
"rights": "full-control",
"apply-to": ["this-folder", "sub-folders", "files"]
}
postDACLPolicyTarget: {
"policy-name": "{{ qtree_name }}",
"path": "/{{ volume_name }}/{{ qtree_name }}",
"ntfs-sd": ["{{ qtree_name }}"],
"ntfs-mode": "propagate",
"security-type": "ntfs"
}
postDACLPolicyApply: {
"policy-name": "{{ qtree_name }}"
}