├── cpuload ├── do_load.sh ├── azurermconfig.json.tmpl ├── .gitignore ├── getnics.py ├── random-load.py ├── README.md └── addautoscalerules.py ├── docs ├── jumpbox.png ├── vipswap.png ├── cpu_graph.png ├── vmssextn_show.png └── vmssupgrade-screenshot.png ├── rollingupgrade ├── vmss.ico ├── vmssconfig.json.tmpl ├── subscription.py ├── vmss.py └── rollingupgrade.py ├── vmssextn ├── vmssconfig.json.tmpl ├── lapextension.json ├── bottleextension.json ├── subscription.py ├── README.md ├── vmssextn.py └── vmss.py ├── jumpbox ├── azurermconfig.json.tmpl ├── .gitignore ├── README.md └── jumpbox.py ├── vipswap ├── azurermconfig.json.tmpl ├── .gitignore ├── README.md └── vip_swap.py ├── vmsscpuplot ├── azurermconfig.json.tmpl ├── .gitignore ├── README.md └── vmss_cpu_plot.py ├── vmssupgrade ├── vmssconfig.json.tmpl ├── README.md └── vmssupgrade.py ├── vmssvmname ├── README.md └── vmssvmname.py ├── .gitattributes ├── .gitignore ├── LICENSE.txt └── README.md /cpuload/do_load.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /usr/bin/python3 ./random-load.py > random.out 2>&1 -------------------------------------------------------------------------------- /docs/jumpbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/docs/jumpbox.png -------------------------------------------------------------------------------- /docs/vipswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/docs/vipswap.png -------------------------------------------------------------------------------- /docs/cpu_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/docs/cpu_graph.png -------------------------------------------------------------------------------- /docs/vmssextn_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/docs/vmssextn_show.png -------------------------------------------------------------------------------- /rollingupgrade/vmss.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/rollingupgrade/vmss.ico -------------------------------------------------------------------------------- /docs/vmssupgrade-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbowerman/vmsstools/HEAD/docs/vmssupgrade-screenshot.png -------------------------------------------------------------------------------- /vmssextn/vmssconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /cpuload/azurermconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /jumpbox/azurermconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /rollingupgrade/vmssconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /vipswap/azurermconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /vmsscpuplot/azurermconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /vmssupgrade/vmssconfig.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "tenantId": "your_tenant_id", 3 | "appId": "your_app_id", 4 | "appSecret": "your_app_secret", 5 | "subscriptionId": "your_sub_id" 6 | } -------------------------------------------------------------------------------- /vmssvmname/README.md: -------------------------------------------------------------------------------- 1 | # vmssvmname 2 | 3 | Python 3 script to convert Azure VM scale set VM hostnames into a VM ids. E.g. 4 | 5 | ``` 6 | > python vmssvmname.py --vmid 1146 --prefix myvmprefix 7 | hostname = myvmprefix0000VU 8 | 9 | > python vmssvmname.py --hostname myvmprefix0000VU 10 | VM ID = 1146 11 | ``` 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /vmssextn/lapextension.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "autoUpgradeMinorVersion": true, 4 | "settings": { 5 | "commandToExecute": "bash installserver.sh", 6 | "fileUris": [ 7 | "https://raw.githubusercontent.com/gbowerman/azure-myriad/master/vmss-ubuntu-scale/installserver.sh", 8 | "https://raw.githubusercontent.com/gbowerman/azure-myriad/master/vmss-ubuntu-scale/workserver.py" 9 | ] 10 | }, 11 | "typeHandlerVersion": "1.4", 12 | "publisher": "Microsoft.OSTCExtensions", 13 | "type": "CustomScriptForLinux" 14 | }, 15 | "name": "lapextension" 16 | } -------------------------------------------------------------------------------- /vmssextn/bottleextension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bottleextension", 3 | "properties": { 4 | "publisher": "Microsoft.Azure.Extensions", 5 | "type": "CustomScript", 6 | "typeHandlerVersion": "2.0", 7 | "autoUpgradeMinorVersion": true, 8 | "settings": { 9 | "fileUris": [ 10 | "https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/201-vmss-bottle-autoscale/installserver.sh", 11 | "https://raw.githubusercontent.com/gbowerman/azure-myriad/master/bigtest/workserver.py" 12 | ], 13 | "commandToExecute": "bash installserver.sh" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Application files 2 | vmssconfig.json 3 | 4 | # Python cache 5 | __pycache__ 6 | 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | 26 | # ========================= 27 | # Operating System Files 28 | # ========================= 29 | 30 | # OSX 31 | # ========================= 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | -------------------------------------------------------------------------------- /cpuload/.gitignore: -------------------------------------------------------------------------------- 1 | # Application files 2 | azurermconfig.json 3 | 4 | # Python cache 5 | __pycache__ 6 | 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | 26 | # ========================= 27 | # Operating System Files 28 | # ========================= 29 | 30 | # OSX 31 | # ========================= 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | -------------------------------------------------------------------------------- /jumpbox/.gitignore: -------------------------------------------------------------------------------- 1 | # Application files 2 | azurermconfig.json 3 | 4 | # Python cache 5 | __pycache__ 6 | 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | 26 | # ========================= 27 | # Operating System Files 28 | # ========================= 29 | 30 | # OSX 31 | # ========================= 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | -------------------------------------------------------------------------------- /vipswap/.gitignore: -------------------------------------------------------------------------------- 1 | # Application files 2 | azurermconfig.json 3 | 4 | # Python cache 5 | __pycache__ 6 | 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | 26 | # ========================= 27 | # Operating System Files 28 | # ========================= 29 | 30 | # OSX 31 | # ========================= 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | -------------------------------------------------------------------------------- /vmsscpuplot/.gitignore: -------------------------------------------------------------------------------- 1 | # Application files 2 | azurermconfig.json 3 | 4 | # Python cache 5 | __pycache__ 6 | 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | 26 | # ========================= 27 | # Operating System Files 28 | # ========================= 29 | 30 | # OSX 31 | # ========================= 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Guy Bowerman 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /vmsscpuplot/README.md: -------------------------------------------------------------------------------- 1 | ## vmss_cpu_plot 2 | 3 | Shows CPU usage graph for a VM scale set. 4 | 5 | ![CPU graph screenshot](../docs/cpu_graph.png) 6 | 7 | Usage: vmss_cpu_plot.py [-h] --vmss VMSS --resourcegroup RESOURCE_GROUP [--verbose] 8 | 9 | Graphs CPU usage for the named scale set for the last hour. 10 | 11 | ### Installation 12 | 1. Install Python 3.x. 13 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed), and matplotlib library. 14 | 3. Clone this repo locally. 15 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 16 | 6. Edit azurermconfig.json in the local directory (rename azurermconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). 17 | 18 | 19 | -------------------------------------------------------------------------------- /cpuload/getnics.py: -------------------------------------------------------------------------------- 1 | # getnics.py - script to get the internal IP addresses from the VMs in a scale set 2 | # to do: do not hardcode the rgname and vmssname - instead make them command line 3 | # parameters 4 | import azurerm 5 | import json 6 | 7 | # Load Azure app defaults 8 | try: 9 | with open('azurermconfig.json') as configFile: 10 | configData = json.load(configFile) 11 | except FileNotFoundError: 12 | print("Error: Expecting azurermconfig.json in current folder") 13 | sys.exit() 14 | 15 | tenant_id = configData['tenantId'] 16 | app_id = configData['appId'] 17 | app_secret = configData['appSecret'] 18 | subscription_id = configData['subscriptionId'] 19 | rgname = 'sgelastic' 20 | vmssname = 'sgelastic' 21 | 22 | # authenticate 23 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 24 | 25 | nics = azurerm.get_vmss_nics(access_token, subscription_id, rgname, vmssname) 26 | for nic in nics['value']: 27 | #print(json.dumps(nic, sort_keys=False, indent=2, separators=(',', ': '))) 28 | ipaddress = nic['properties']['ipConfigurations'][0]['properties']['privateIPAddress'] 29 | print(ipaddress) -------------------------------------------------------------------------------- /cpuload/random-load.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | import requests 3 | import sys 4 | import time 5 | 6 | PORT = 9000 7 | 8 | def ipaddr_on(ipaddr): 9 | url = 'http://' + ipaddr + ':' + str(PORT) + '/do_work' 10 | requests.get(url) 11 | 12 | def ipaddr_off(ipaddr): 13 | url = 'http://' + ipaddr + ':' + str(PORT) + '/stop_work' 14 | requests.get(url) 15 | 16 | # load ip addresses 17 | try: 18 | addrlist = open('ipaddrs').read().splitlines() 19 | except FileNotFoundError: 20 | print("Error: Expecting file ipaddrs in current folder") 21 | sys.exit() 22 | 23 | total_machines = len(addrlist) 24 | 25 | # main loop 26 | while True: 27 | # pick a random time 28 | loop_time = randint(180, 1200) # random time between 3 and 20 mins 29 | print('Run for ' + str(loop_time) + ' seconds') 30 | 31 | # pick a random number of machines between 20 and 100% of total 32 | machine_count = randint(total_machines//5, total_machines) 33 | print('Hit ' + str(machine_count) + ' machines') 34 | 35 | # activate that many machines 36 | for i in range(machine_count): 37 | ipaddr_on(addrlist[i]) 38 | time.sleep(loop_time) 39 | # after time interval switch them off 40 | for j in range(machine_count): 41 | ipaddr_off(addrlist[j]) 42 | -------------------------------------------------------------------------------- /vipswap/README.md: -------------------------------------------------------------------------------- 1 | ## vipswap 2 | 3 | Swaps the public IP addresses between two Azure load balancers. 4 | 5 | ``` 6 | usage: vip_swap.py [-h] --resourcegroup RESOURCE_GROUP --lb1 LB1 --lb2 LB2 7 | [--verbose] [-y] 8 | The following arguments are required: --resourcegroup/-g, --lb1/-1, --lb2/-2 9 | ``` 10 | 11 | ![vipswap screenshot](../docs/vipswap.png) 12 | 13 | See also [VIP Swap – blue-green deployment in Azure Resource Manager](https://msftstack.wordpress.com/2017/02/24/vip-swap-blue-green-deployment-in-azure-resource-manager/) 14 | 15 | ### Installation 16 | 1. Install Python 3.x. 17 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed). 18 | 3. Clone this repo locally. 19 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 20 | 6. Edit azurermconfig.json in the local directory (rename azurermconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). -------------------------------------------------------------------------------- /vmssextn/subscription.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azurerm 4 | 5 | 6 | # Azure subscription class 7 | class subscription(): 8 | def __init__(self, tenant_id, app_id, app_secret, subscription_id): 9 | self.sub_id = subscription_id 10 | self.tenant_id = tenant_id 11 | self.app_id = app_id 12 | self.app_secret = app_secret 13 | 14 | self.access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 15 | 16 | # update the authentication token for this subscription 17 | def auth(self): 18 | self.access_token = azurerm.get_access_token(self.tenant_id, self.app_id, self.app_secret) 19 | return self.access_token 20 | 21 | # list VM Scale Sets in this subscription - names only 22 | def get_vmss_list(self): 23 | vmss_sub_list = azurerm.list_vmss_sub(self.access_token, self.sub_id) 24 | self.vmsslist = [] 25 | self.vmssdict = {} 26 | # build a simple list of VM Scale Set names and a dictionary of VMSS model views 27 | try: 28 | for vmss in vmss_sub_list['value']: 29 | vmssname = vmss['name'] 30 | self.vmsslist.append(vmssname) 31 | self.vmssdict[vmssname] = vmss 32 | 33 | except KeyError: 34 | self.status = 'KeyError: azurerm.list_vmss_sub() returned: ' + json.dumps(vmss_sub_list) 35 | return self.vmsslist 36 | -------------------------------------------------------------------------------- /rollingupgrade/subscription.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azurerm 4 | 5 | 6 | # Azure subscription class 7 | class subscription(): 8 | def __init__(self, tenant_id, app_id, app_secret, subscription_id): 9 | self.sub_id = subscription_id 10 | self.tenant_id = tenant_id 11 | self.app_id = app_id 12 | self.app_secret = app_secret 13 | 14 | self.access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 15 | 16 | # update the authentication token for this subscription 17 | def auth(self): 18 | self.access_token = azurerm.get_access_token(self.tenant_id, self.app_id, self.app_secret) 19 | return self.access_token 20 | 21 | # list VM Scale Sets in this subscription - names only 22 | def get_vmss_list(self): 23 | vmss_sub_list = azurerm.list_vmss_sub(self.access_token, self.sub_id) 24 | self.vmsslist = [] 25 | self.vmssdict = {} 26 | # build a simple list of VM Scale Set names and a dictionary of VMSS model views 27 | try: 28 | for vmss in vmss_sub_list['value']: 29 | vmssname = vmss['name'] 30 | self.vmsslist.append(vmssname) 31 | self.vmssdict[vmssname] = vmss 32 | 33 | except KeyError: 34 | self.status = 'KeyError: azurerm.list_vmss_sub() returned: ' + json.dumps(vmss_sub_list) 35 | return self.vmsslist 36 | -------------------------------------------------------------------------------- /cpuload/README.md: -------------------------------------------------------------------------------- 1 | # Random load generator for Azure VM scale set VMs 2 | A set of scripts to trigger a random CPU load against VMs in a scale set. This works by creating a standalone VM in the scale VNet as the scale set, and using this VM to query the internal IP addresses and send a load to them. Tested up to 1000 VMs. 3 | 4 | To generate a load: 5 | 6 | 1. Deploy a VM scale set which includes a custom script extension which deploys a Python bottle server: [https://github.com/gbowerman/azure-myriad/blob/master/bigtest/bigbottle.json](https://github.com/gbowerman/azure-myriad/blob/master/bigtest/bigbottle.json) 7 | 2. Deploy a jumpbox VM in the same subnet. E.g. use [https://github.com/gbowerman/vmsstools/tree/master/jumpbox](https://github.com/gbowerman/vmsstools/tree/master/jumpbox) 8 | 3. ssh to the jumpbox VM and use the getnics.py script to generate a list of VMSS VM internal IP addresses. 9 | 10 | Note: This requires an _azurermconfig.json_ file in the same directory, which contains your service principal app/tenant details and subscription id. See azurermconfig.json.tmpl in this repo as an example. 11 | 12 | Note: getnics.py currently has a hardcoded resource group and scale set name. You'll need to edit getnics.py to set your resource group and scale set. 13 | ``` 14 | sudo apt install python3-pip 15 | sudo pip3 install azurerm 16 | curl https://raw.githubusercontent.com/gbowerman/vmsstools/master/cpuload/getnics.py > getnics.py 17 | curl https://raw.githubusercontent.com/gbowerman/vmsstools/master/cpuload/random-load.py > random-load.py 18 | vi getnics.py # put your RG and VMSS name here 19 | python3 getnics.py > ipaddrs 20 | ``` 21 | 4. Start a load running in background with 22 | ``` 23 | nohup python3 random-load.py > random.out 2>&1 & 24 | ``` 25 | 5. Kill the process when you're done. 26 | -------------------------------------------------------------------------------- /jumpbox/README.md: -------------------------------------------------------------------------------- 1 | ## jumpbox 2 | 3 | Creates a new VM in a resource group. You can use this to create a VM in an empty resource group if you want, or use it to create a jump box in an existing VNET to be used to connect to existing resources in the VNET like VM scale set VMs. 4 | 5 | ``` 6 | usage: jumpbox.py [-h] --vmname VMNAME --rgname RGNAME [--user USER] 7 | [--password PASSWORD] [--sshkey SSHKEY] [--sshpath SSHPATH] 8 | [--location LOCATION] [--dns DNS] [--vnet VNET] [--nowait] 9 | [--nonsg] [--verbose] 10 | 11 | The following arguments are required: --vmname/-n, --rgname/-g 12 | ``` 13 | 14 | This tool will create a VM in the first VNET it finds in the resource group (or the VNET you specify). If it doesn't find a VNET it will create one. 15 | 16 | You can provide a user and password/public key/public key file. If no authentication method provided and no default public key file found it will create a password for you. 17 | 18 | By default jumpbox.py will wait for the VM to be provisioned, unless you specify the --nowait argument. 19 | 20 | ![jumpbox screenshot](../docs/jumpbox.png) 21 | 22 | ### Installation 23 | 1. Install Python 3.x. 24 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed). 25 | 3. Clone this repo locally. 26 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 27 | 6. Edit azurermconfig.json in the local directory (rename azurermconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). -------------------------------------------------------------------------------- /vmssvmname/vmssvmname.py: -------------------------------------------------------------------------------- 1 | # vmssvmname.py - convert a VMSS VM hostname into a VM id and vice versa 2 | # VM references can look like: 3 | # instanceId = 1146 4 | # name = myvmss_1146 5 | # hostname = vmssprefix0000VU 6 | import argparse 7 | import sys 8 | 9 | 10 | def hostname_to_vmid(hostname): 11 | # get last 6 characters and remove leading zeroes 12 | hexatrig = hostname[-6:].lstrip('0') 13 | multiplier = 1 14 | vmid = 0 15 | # reverse string and process each char 16 | for x in hexatrig[::-1]: 17 | if x.isdigit(): 18 | vmid += int(x) * multiplier 19 | else: 20 | # convert letter to corresponding integer 21 | vmid += (ord(x) - 55) * multiplier 22 | multiplier *= 36 23 | return vmid 24 | 25 | 26 | def vmid_to_hostname(vmid, prefix): 27 | hexatrig = '' 28 | # convert decimal vmid to hexatrigesimal base36 29 | while vmid > 0: 30 | vmid_mod = vmid % 36 31 | # convert int to corresponding letter 32 | if vmid_mod > 9: 33 | char = chr(vmid_mod + 55) 34 | else: 35 | char = str(vmid_mod) 36 | hexatrig = char + hexatrig 37 | vmid = int(vmid/36) 38 | return prefix + hexatrig.zfill(6) 39 | 40 | 41 | # validate command line arguments 42 | argParser = argparse.ArgumentParser() 43 | 44 | argParser.add_argument('--hostname', '-n', required=False, 45 | action='store', help='VMSS VM hostname') 46 | argParser.add_argument('--prefix', '-p', required=False, 47 | action='store', help='VMSS machine name prefix') 48 | argParser.add_argument('--vmid', '-i', type=int, required=False, 49 | action='store', help='VM ID') 50 | args = argParser.parse_args() 51 | 52 | hostname = args.hostname 53 | vmid = args.vmid 54 | prefix = args.prefix 55 | 56 | if hostname is None: 57 | if vmid is None or prefix is None: 58 | sys.exit('Error: Provide a value for --hostname, or --vmid and --prefix.') 59 | hostname = vmid_to_hostname(vmid, prefix) 60 | print('hostname = ' + hostname) 61 | else: 62 | if vmid is not None: 63 | sys.exit('Error: Provide a value for --hostname, or --vmid and --prefix. Not both.') 64 | vmid = hostname_to_vmid(hostname) 65 | print('VM ID = ' + str(vmid)) 66 | -------------------------------------------------------------------------------- /cpuload/addautoscalerules.py: -------------------------------------------------------------------------------- 1 | # addautoscalerules.py - example program to add autoscale rules to a VM scale set 2 | # in this example the resource group, scale set, and rules are hardcoded 3 | # to do: make this into a generic program to turn a regular scale set into an autoscale scale set 4 | import json 5 | import azurerm 6 | 7 | # Load Azure app defaults 8 | try: 9 | with open('azurermconfig.json') as configFile: 10 | configData = json.load(configFile) 11 | except FileNotFoundError: 12 | print("Error: Expecting azurermconfig.json in current folder") 13 | sys.exit() 14 | 15 | tenant_id = configData['tenantId'] 16 | app_id = configData['appId'] 17 | app_secret = configData['appSecret'] 18 | subscription_id = configData['subscriptionId'] 19 | 20 | # hardcode vmss parameters for now 21 | rgname = 'sgelastic' 22 | vmssname = 'sgelastic' 23 | 24 | # authenticate 25 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 26 | 27 | # figure out location 28 | try: 29 | rg = azurerm.get_resource_group(access_token, subscription_id, rgname) 30 | location = rg['location'] 31 | except KeyError: 32 | print('Cannot find resource group ' + rgname + '. Check connection/authorization.') 33 | print(json.dumps(rg, sort_keys=False, indent=2, separators=(',', ': '))) 34 | sys.exit() 35 | print('location = ' + location) 36 | 37 | # create autoscale rule 38 | print('Creating autoscale rules') 39 | metric_name = 'Percentage CPU' 40 | operator = 'GreaterThan' 41 | threshold = 60 42 | direction = 'Increase' 43 | change_count = 1 44 | rule1 = azurerm.create_autoscale_rule(subscription_id, rgname, vmssname, metric_name, operator, \ 45 | threshold, direction, change_count) 46 | threshold = 10 47 | operator = 'LessThan' 48 | direction = 'Decrease' 49 | rule2 = azurerm.create_autoscale_rule(subscription_id, rgname, vmssname, metric_name, operator, \ 50 | threshold, direction, change_count) 51 | rules = [rule1, rule2] 52 | # print(json.dumps(rules, sort_keys=False, indent=2, separators=(',', ': '))) 53 | 54 | # create autoscale setting 55 | setting_name = "SGELASTIC autoscale settings" 56 | print('Creating autoscale setting: ' + setting_name) 57 | min = 999 58 | max = 1000 59 | default = 1000 60 | response = azurerm.create_autoscale_setting(access_token, subscription_id, rgname, setting_name, \ 61 | vmssname, location, min, max, default, rules) 62 | if response.status_code != 201: 63 | print("Autoscale setting create error: " + str(response.status_code)) 64 | else: 65 | print("Autoscale settings created.") 66 | -------------------------------------------------------------------------------- /vmssextn/README.md: -------------------------------------------------------------------------------- 1 | ## vmssextn 2 | 3 | Tool to read, add, remove extensions from an Azure VM Scale Set. 4 | 5 | ![vmssextn screenshot](../docs/vmssextn_show.png) 6 | 7 | This script can: 8 | 9 | 1. Show the extensions which are installed in a Virtual Machine Scale Set. 10 | 2. Delete an extension from a VM Scale Set by name. 11 | 3. Add an extension to a VM Scale Set by providing a JSON file containing the properties. 12 | 13 | Note: this tool just updates the VMSS model. After that, if the VMSS 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 14 | 15 | ### Showing which extensions are installed 16 | 17 | Lists the extensions installed in a specified scale set, including name, publisher, version. 18 | 19 | Use –verbose to dump the JSON properties, which could then be saved to a file and used to add extensions to other scale sets. 20 | 21 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest 22 | 23 | ### Adding 24 | 25 | Add an extension to a VM Scale Set by specifying a file containing an extension definition in JSON format. 26 | 27 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --add extnfile 28 | 29 | For an example JSON extension definition file to add, see: [lapextension.json](./lapextension.json) 30 | 31 | Note: this tool just updates the VMSS model. After that, if the VMSS the 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 32 | 33 | ### Deleting an extension 34 | 35 | Delete a VM extension from a VM Scale Set by name. 36 | 37 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --delete extnname 38 | 39 | Note: this tool just updates the VMSS model. After that, if the VMSS 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 40 | 41 | ### Updating an extension 42 | 43 | Updates a VM extension from a VM Scale Set by name using a JSON definition in a file. 44 | 45 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --update extnfile 46 | 47 | 48 | 49 | ### Installation 50 | 51 | 1. Install Python 3.x. 52 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed) 53 | 3. Clone this repo locally. In particular copy the vmssextn folder. 54 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 55 | 6. Edit vmssconfig.json in the local directory (rename vmssconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). 56 | 7. Run the command, e.g. python vmssextn.py -------------------------------------------------------------------------------- /vmsscpuplot/vmss_cpu_plot.py: -------------------------------------------------------------------------------- 1 | # vmss_cpu_plot.py 2 | # - prints most recent CPU host metrics for the named scale set 3 | # To use this program without modification you need vmssconfig.json 4 | # in the local folder containing: 5 | # - tenant id 6 | # - application id 7 | # - application secret 8 | # - subscription id 9 | import argparse 10 | import azurerm 11 | import datetime 12 | import json 13 | import matplotlib.pyplot as pyplot 14 | import matplotlib.dates as dates 15 | import sys 16 | 17 | def main(): 18 | # create argument parser 19 | argParser = argparse.ArgumentParser() 20 | 21 | argParser.add_argument('--vmss', '-n', required=True, action='store', \ 22 | help='Scale set name') 23 | argParser.add_argument('--resourcegroup', '-g', required=True, dest='resource_group', \ 24 | action='store', help='Resource group name') 25 | argParser.add_argument('--verbose', '-v', action='store_true', default=False, \ 26 | help='Print verbose metrics output to console') 27 | 28 | args = argParser.parse_args() 29 | 30 | verbose = args.verbose # dump metrics JSON output to console when True 31 | vmssname = args.vmss 32 | rgname = args.resource_group 33 | 34 | # Load Azure app defaults 35 | try: 36 | with open('azurermconfig.json') as configFile: 37 | configData = json.load(configFile) 38 | except FileNotFoundError: 39 | print("Error: Expecting azurermonfig.json in current folder") 40 | sys.exit() 41 | 42 | tenant_id = configData['tenantId'] 43 | app_id = configData['appId'] 44 | app_secret = configData['appSecret'] 45 | subscription_id = configData['subscriptionId'] 46 | 47 | # get Azure access token 48 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 49 | 50 | # get host metrics for the named VM scale set 51 | provider = 'Microsoft.Compute' 52 | resource_type = 'virtualMachineScaleSets' 53 | metrics = azurerm.get_metrics_for_resource(access_token, subscription_id, rgname, \ 54 | provider, resource_type, vmssname) 55 | if verbose == True: 56 | print(json.dumps(metrics, sort_keys=False, indent=2, separators=(',', ': '))) 57 | 58 | # sort metrics into CPU and timestamp lists for easy plotting 59 | for metric in metrics['value']: 60 | if metric['name']['value'] == 'Percentage CPU': 61 | timestamp_list = [] 62 | cpu_list = [] 63 | for data in metric['data']: 64 | if 'average' in data: 65 | cpu_list.append(data['average']) 66 | # convert timestamp from 2016-11-26T06:26:00Z to python datetime format 67 | dt = datetime.datetime.strptime(data['timeStamp'], '%Y-%m-%dT%H:%M:%SZ') 68 | timestamp_list.append(dt) 69 | break 70 | 71 | # set figure title and graph style 72 | fig = pyplot.gcf() 73 | fig.canvas.set_window_title('Host metrics graph') 74 | #ig.patch.set_facecolor('white') 75 | pyplot.style.use('ggplot') 76 | #ax = pyplot.gca() 77 | #ax.set_axis_bgcolor('grey') 78 | 79 | # plot values 80 | pyplot.plot(timestamp_list, cpu_list) 81 | pyplot.gcf().autofmt_xdate() 82 | pyplot.ylim([0,100]) 83 | 84 | # label axis and graph 85 | pyplot.ylabel('CPU Percentage') 86 | pyplot.xlabel('Time stamp (UTC)') 87 | pyplot.title('Avg CPU over time for scale set: ' + vmssname) 88 | 89 | # display 90 | pyplot.show() 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /vmssupgrade/README.md: -------------------------------------------------------------------------------- 1 | ## vmssupgrade 2 | 3 | Tool to roll out an OS upgrade to a running VM Scale Set, one upgrade domain at a time. 4 | 5 | This script performs the following steps: 6 | 7 | 1. Query the VM Scale Set model and check the current OS version. 8 | 2. Confirms OS upgrade with the user (unless --noprompt is used). 9 | 3. Updates the VMSS model to the new version. At this point no VMs have been upgraded, unless the VMSS _upgradePolicy_ property is set to "Automatic"). 10 | 4. Does a _manualUpgrade_ on the VMs in the selected update domain (or specific VMs, depending on the command line parameters). 11 | 5. Waits until the VMSS provisioning state has finished updating (unless the --nowait command line parameter is used). 12 | 13 | ![vmssupgrade screenshot](../docs/vmssupgrade-screenshot.png) 14 | 15 | ### Installation 16 | 1. Install Python 3.x. 17 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed) 18 | 3. Clone this repo locally. In particular copy the vmssupgrade folder. 19 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 20 | 6. Edit vmssconfig.json in the local directory (rename vmssconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). 21 | 7. usage: vmssupgrade -r rgname -s vmssname -n newversion {-u updatedomain|-i vmid|-l vmlist} [-y][-v][-h] 22 | 23 | ### Examples 24 | 25 | **Upgrading a single VM in a VM Scale Set** 26 | 27 | This example upgrades VM id #1 and sets verbose mode, which will display the provisioning state every 5 seconds until complete.. 28 | 29 | vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201506100" -i 1 -v 30 | 31 | **Upgrading upgrade domain 0 of a VM Scale Set** 32 | 33 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --updatedomain 0 34 | 35 | **Upgrading a custom image URI in upgrade domain 0 of a VM Scale Set** 36 | 37 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --customuri "path_to_custom_image" --updatedomain 0 38 | 39 | **Start an upgrade but don't wait for it to complete** 40 | 41 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --updatedomain 0 --nowait 42 | 43 | **Upgrade an arbitrary list of VMs in a VM Scale Set** 44 | 45 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --vmlist '["1","2","3","4"]' 46 | 47 | **Quick video walkthrough** 48 | 49 | [![vmssupgrade demo](https://img.youtube.com/vi/0sc9YMgvXLY/0.jpg)](https://www.youtube.com/watch?v=0sc9YMgvXLY) 50 | 51 | ### Notes 52 | 53 | - The ability to update an OS version in a running scale set was only recently enabled in production. Your account may need to be whitelisted if you try this before April 23 2016. Contact @gbowerman for more details. 54 | 55 | - This script is light on error checking and should be thought of as a proof of concept. If you're thinking of using it in production you'd need to do some work on it to improve error handling and logging. 56 | 57 | - This script is designed to work with both platform images (updates the _version_ property of the _imageReference_) and custom images (updating the _osDisk->image->Uri_), though custom image support hasn't been tested yet. Let me know if it works :-) 58 | 59 | - You can update the version of a platform image. Updating the _sku_ (for example going from Ubuntu 15.10 to 16.04) is not possible. This maybe possible for VM Scale Sets in the future. 60 | -------------------------------------------------------------------------------- /vmssextn/vmssextn.py: -------------------------------------------------------------------------------- 1 | # Python script to do read/install/delete VM extensions on VM Scale Sets 2 | # usage: vmssextn -r rgname -v vmssname [--delete extnname] [--add extnfile] [-y][--verbose] 3 | 4 | import argparse 5 | import json 6 | import sys 7 | 8 | import subscription 9 | import vmss 10 | 11 | 12 | def main(): 13 | # create parser 14 | argParser = argparse.ArgumentParser() 15 | 16 | argParser.add_argument('--vmssname', '-s', required=True, action='store', help='VM Scale Set name') 17 | argParser.add_argument('--resourcegroup', '-r', required=True, dest='resource_group', action='store', 18 | help='Resource group name') 19 | argParser.add_argument('--delete', '-d', dest='extnname', action='store', help='Name of extension to delete') 20 | argParser.add_argument('--add', '-a', dest='extnfile', action='store', 21 | help='File containing extension defintion to add') 22 | argParser.add_argument('--update', '-u', dest='extnufile', action='store', 23 | help='File containing extension defintion to update') 24 | argParser.add_argument('--verbose', '-v', action='store_true', default=False, help='Print full extension definition') 25 | 26 | args = argParser.parse_args() 27 | 28 | # switches to determine program behavior 29 | verbose = args.verbose # print extra status information when True 30 | vmssname = args.vmssname 31 | resource_group = args.resource_group 32 | if args.extnname is not None: 33 | extnname = args.extnname 34 | mode = 'delete' 35 | elif args.extnfile is not None: 36 | extnfile = args.extnfile 37 | mode = 'add' 38 | elif args.extnufile is not None: 39 | extnfile = args.extnufile 40 | mode = 'update' 41 | else: 42 | mode = 'report' 43 | 44 | # Load Azure app defaults 45 | try: 46 | with open('vmssconfig.json') as configFile: 47 | configData = json.load(configFile) 48 | except FileNotFoundError: 49 | print("Error: Expecting vmssconfig.json in current folder") 50 | sys.exit() 51 | 52 | sub = subscription.subscription(configData['tenantId'], configData['appId'], configData['appSecret'], 53 | configData['subscriptionId']) 54 | sub.get_vmss_list() 55 | current_vmss = vmss.vmss(vmssname, sub.vmssdict[vmssname], sub.sub_id, sub.access_token) 56 | # print(json.dumps(vmssmodel, sort_keys=False, indent=2, separators=(',', ': '))) 57 | 58 | # start by getting the extension list 59 | try: 60 | extnprofile = current_vmss.model['properties']['virtualMachineProfile']['extensionProfile'] 61 | except KeyError: 62 | if mode != 'add': 63 | print('Scale Set: ' + vmssname + ' does not have any extensions defined.') 64 | sys.exit() 65 | else: 66 | extnprofile = None 67 | 68 | if mode == 'report': 69 | # print selected details about each extension 70 | print('Found the following extensions in scale set: ' + vmssname) 71 | 72 | for extension in extnprofile['extensions']: 73 | print('\nName: ' + extension['name']) 74 | print('Type: ' + extension['properties']['type']) 75 | print('Publisher: ' + extension['properties']['publisher']) 76 | print('Version: ' + extension['properties']['typeHandlerVersion']) 77 | if verbose: 78 | print('Extension definition:\n') 79 | print(json.dumps(extension, sort_keys=False, indent=2, separators=(',', ': '))) 80 | sys.exit() 81 | 82 | elif mode == 'delete': 83 | index = 0 84 | extn_index = -1 85 | for extension in extnprofile['extensions']: 86 | if extension['name'] == extnname: 87 | extn_index = index 88 | index += 1 89 | if extn_index > -1: 90 | # delete the extension from the list 91 | del extnprofile['extensions'][extn_index] 92 | else: 93 | print('Extension ' + extnname + ' not found.') 94 | sys.exit() 95 | 96 | elif mode == 'add': 97 | # load the extension definition file 98 | try: 99 | with open(extnfile) as extension_file: 100 | extndata = json.load(extension_file) 101 | except FileNotFoundError: 102 | print("Error: Expecting ' + extnfile + ' in current folder (or absolute path)") 103 | sys.exit() 104 | 105 | if extnprofile is None: 106 | # create an extensionProfile 107 | extnprofile = {'extensions':[]} 108 | # add the extension definition to the list 109 | extnprofile['extensions'].append(extndata) 110 | 111 | elif mode == 'update': 112 | # load the extension definition file 113 | try: 114 | with open(extnfile) as extension_file: 115 | extndata = json.load(extension_file) 116 | except FileNotFoundError: 117 | print("Error: Expecting ' + extnfile + ' in current folder (or absolute path)") 118 | sys.exit() 119 | 120 | # get the extension name to update from the file 121 | extnname = extndata['name'] 122 | 123 | # for update, first delete the extension, from the list then add 124 | index = 0 125 | extn_index = -1 126 | for extension in extnprofile['extensions']: 127 | if extension['name'] == extnname: 128 | extn_index = index 129 | index += 1 130 | if extn_index > -1: 131 | # delete the extension from the list 132 | del extnprofile['extensions'][extn_index] 133 | else: 134 | print('Extension ' + extnname + ' not found.') 135 | sys.exit() 136 | # add the extension definition to the list 137 | extnprofile['extensions'].append(extndata) 138 | 139 | # update and apply model with the new extensionProfile 140 | current_vmss.update_extns(extnprofile) 141 | print('VMSS update status: ' + str(current_vmss.status.status_code)) 142 | if current_vmss.status.status_code == 200: 143 | if current_vmss.upgradepolicy == 'Manual': 144 | print('Update in progress. Once model update completes, apply manualUpgrade to VMs.') 145 | print("Note: You won't be able re-add an extension of the same name until the new model is applied to all VMs.") 146 | else: 147 | print('Scale Set update in progress.') 148 | else: 149 | print(current_vmss.status.text) 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /vipswap/vip_swap.py: -------------------------------------------------------------------------------- 1 | # VIP swap test script 2 | # requires 2 load balancers, in the same resource group, with public IP addresses 3 | # can be used to swap a staging scale application into production 4 | 5 | import azurerm 6 | import argparse 7 | import json 8 | import sys 9 | from haikunator import Haikunator # used to create a random ip name for the floating ip 10 | import time 11 | 12 | def handle_bad_update(operation, ret): 13 | print("Error " + operation) 14 | print('Return code: ' + str(ret.status_code) + ' Error: ' + ret.text) 15 | sys.exit(1) 16 | 17 | 18 | def main(): 19 | # create parser 20 | argParser = argparse.ArgumentParser() 21 | 22 | # arguments: resource group lb name 1, 2 23 | argParser.add_argument('--resourcegroup', '-g', required=True, dest='resource_group', action='store', help='Resource group name') 24 | argParser.add_argument('--lb1', '-1', required=True, action='store', help='Load balancer 1 name') 25 | argParser.add_argument('--lb2', '-2', required=True, action='store', help='Load balancer 2 name') 26 | 27 | argParser.add_argument('--verbose', '-v', action='store_true', default=False, help='Show additional information') 28 | argParser.add_argument('-y', dest='noprompt', action='store_true', default=False, help='Do not prompt for confirmation') 29 | 30 | args = argParser.parse_args() 31 | 32 | verbose = args.verbose # print extra status information when True 33 | 34 | resource_group = args.resource_group 35 | lb1 = args.lb1 36 | lb2 = args.lb2 37 | 38 | # Load Azure app defaults 39 | try: 40 | with open('azurermconfig.json') as configFile: 41 | configdata = json.load(configFile) 42 | except FileNotFoundError: 43 | print("Error: Expecting lbconfig.json in current folder") 44 | sys.exit() 45 | 46 | tenant_id = configdata['tenantId'] 47 | app_id = configdata['appId'] 48 | app_secret = configdata['appSecret'] 49 | subscription_id = configdata['subscriptionId'] 50 | 51 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 52 | 53 | # figure out location of resource group and use that for the float ip 54 | rg = azurerm.get_resource_group(access_token, subscription_id, resource_group) 55 | location = rg['location'] 56 | 57 | # Create a spare public IP address 58 | ip_name = Haikunator().haikunate(delimiter='') 59 | dns_label = ip_name + 'dns' 60 | print('Creating float public IP: ' + ip_name) 61 | ip_ret = azurerm.create_public_ip(access_token, subscription_id, resource_group, ip_name, dns_label, location) 62 | floatip_id = ip_ret.json()['id'] 63 | if verbose is True: 64 | print('Float ip id = ' + floatip_id) 65 | 66 | # 1. Get lb 2 67 | lbmodel2 = azurerm.get_load_balancer(access_token, subscription_id, resource_group, lb2) 68 | lb2_ip_id = lbmodel2['properties']['frontendIPConfigurations'][0]['properties']['publicIPAddress']['id'] 69 | lb2_ip_name = lb2_ip_id.split('publicIPAddresses/',1)[1] 70 | if verbose is True: 71 | print(lb2 + ' ip id: ' + lb2_ip_id) 72 | print(lb2 + ' model:') 73 | print(json.dumps(lbmodel2, sort_keys=False, indent=2, separators=(',', ': '))) 74 | 75 | # 2. Assign new ip to lb 2 76 | print('Updating ' + lb2 + ' ip to float ip: ' + ip_name) 77 | lbmodel2['properties']['frontendIPConfigurations'][0]['properties']['publicIPAddress']['id'] = floatip_id 78 | ret = azurerm.update_load_balancer(access_token, subscription_id, resource_group, lb2, json.dumps(lbmodel2)) 79 | if (ret.status_code != 200): 80 | handle_bad_update("updating " + lb2, ret) 81 | 82 | if verbose is True: 83 | print('original ip id: ' + lb2_ip_id + ', new ip id: ' + floatip_id) 84 | print(json.dumps(ret, sort_keys=False, indent=2, separators=(',', ': '))) 85 | print('Waiting for old ' + lb2 + ' ip: ' + lb2_ip_name + ' to be unnassigned') 86 | waiting = True 87 | start1 = time.time() 88 | while waiting: 89 | lbmodel2 = azurerm.get_load_balancer(access_token, subscription_id, resource_group, lb2) 90 | if lbmodel2['properties']['provisioningState'] == 'Succeeded': 91 | waiting = False 92 | time.sleep(3) 93 | end1 = time.time() 94 | print('Elapsed time: ' + str(int(end1 - start1))) 95 | # 3. Get lb 1 96 | lbmodel1 = azurerm.get_load_balancer(access_token, subscription_id, resource_group, lb1) 97 | lb1_ip_id = lbmodel1['properties']['frontendIPConfigurations'][0]['properties']['publicIPAddress']['id'] 98 | 99 | if verbose is True: 100 | print(lb1 + ' ip id: ' + lb1_ip_id) 101 | print(lb1 + ' model:') 102 | print(json.dumps(lbmodel1, sort_keys=False, indent=2, separators=(',', ': '))) 103 | lb1_ip_name = lb1_ip_id.split('publicIPAddresses/',1)[1] 104 | 105 | # 4. Assign old ip 2 to lb 1 106 | print('Downtime begins: Updating ' + lb1 + ' ip to ' + lb2_ip_name) 107 | start2 = time.time() 108 | lbmodel1['properties']['frontendIPConfigurations'][0]['properties']['publicIPAddress']['id'] = lb2_ip_id 109 | ret = azurerm.update_load_balancer(access_token, subscription_id, resource_group, lb1, json.dumps(lbmodel1)) 110 | if (ret.status_code != 200): 111 | handle_bad_update("updating " + lb1, ret) 112 | if verbose is True: 113 | print(json.dumps(ret, sort_keys=False, indent=2, separators=(',', ': '))) 114 | 115 | print('Waiting for old ' + lb1 + ' ip: ' + lb1_ip_name + ' to be unnassigned') 116 | waiting = True 117 | while waiting: 118 | lbmodel1 = azurerm.get_load_balancer(access_token, subscription_id, resource_group, lb1) 119 | if lbmodel1['properties']['provisioningState'] == 'Succeeded': 120 | waiting = False 121 | time.sleep(3) 122 | end2 = time.time() 123 | print('Staging IP ' + lb2_ip_name + ' now points to old production LB ' + lb1) 124 | print('Elapsed time: ' + str(int(end2 - start1))) 125 | 126 | # 5. Assign old ip 1 to lb 2 127 | print('Updating ' + lb2 + ' ip to ' + lb1_ip_name) 128 | lbmodel2['properties']['frontendIPConfigurations'][0]['properties']['publicIPAddress']['id'] = lb1_ip_id 129 | ret = azurerm.update_load_balancer(access_token, subscription_id, resource_group, lb2, json.dumps(lbmodel2)) 130 | if (ret.status_code != 200): 131 | handle_bad_update("updating " + lb2, ret) 132 | 133 | if verbose is True: 134 | print('Original ip id: ' + lb2_ip_id + ', new ip id: ' + lb1_ip_id) 135 | print(json.dumps(ret, sort_keys=False, indent=2, separators=(',', ': '))) 136 | print('Waiting for ' + lb2 + ' provisioning to complete') 137 | waiting = True 138 | while waiting: 139 | lbmodel2 = azurerm.get_load_balancer(access_token, subscription_id, resource_group, lb2) 140 | if lbmodel2['properties']['provisioningState'] == 'Succeeded': 141 | waiting = False 142 | time.sleep(3) 143 | end3 = time.time() 144 | 145 | # 6. Delete floatip 146 | print('VIP swap complete') 147 | print('Downtime: ' + str(int(end3 - start2)) + '. Total elapsed time: ' + \ 148 | str(int(end3 - start1))) 149 | print('Deleting float ip: ' + ip_name) 150 | azurerm.delete_public_ip(access_token, subscription_id, resource_group, ip_name) 151 | 152 | if __name__ == "__main__": 153 | main() -------------------------------------------------------------------------------- /rollingupgrade/vmss.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azurerm 4 | 5 | 6 | class vmss(): 7 | def __init__(self, vmssname, vmssmodel, subscription_id, access_token): 8 | self.name = vmssname 9 | id = vmssmodel['id'] 10 | self.rgname = id[id.index('resourceGroups/') + 15:id.index('/providers')] 11 | self.sub_id = subscription_id 12 | self.access_token = access_token 13 | 14 | self.model = vmssmodel 15 | self.adminuser = vmssmodel['properties']['virtualMachineProfile']['osProfile']['adminUsername'] 16 | self.capacity = vmssmodel['sku']['capacity'] 17 | self.location = vmssmodel['location'] 18 | self.nameprefix = vmssmodel['properties']['virtualMachineProfile']['osProfile']['computerNamePrefix'] 19 | self.overprovision = vmssmodel['properties']['overprovision'] 20 | self.tier = vmssmodel['sku']['tier'] 21 | self.upgradepolicy = vmssmodel['properties']['upgradePolicy']['mode'] 22 | self.vmsize = vmssmodel['sku']['name'] 23 | 24 | # if it's a platform image, the model will have these 25 | if 'imageReference' in vmssmodel['properties']['virtualMachineProfile']['storageProfile']: 26 | self.image_type = 'platform' 27 | self.offer = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['offer'] 28 | self.sku = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['sku'] 29 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] 30 | # else it's a custom image it will have an image URI - to do: add something to display the image URI 31 | else: 32 | # for now just set these values so it doesn't break 33 | self.image_type = 'custom' 34 | self.offer = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['osType'] 35 | self.sku = 'custom' 36 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] 37 | 38 | self.provisioningState = vmssmodel['properties']['provisioningState'] 39 | self.status = self.provisioningState 40 | 41 | # update the model, useful to see if provisioning is complete 42 | def refresh_model(self): 43 | vmssmodel = azurerm.get_vmss(self.access_token, self.sub_id, self.rgname, self.name) 44 | self.model = vmssmodel 45 | self.capacity = vmssmodel['sku']['capacity'] 46 | self.vmsize = vmssmodel['sku']['name'] 47 | if self.image_type == 'platform': 48 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] 49 | else: 50 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] 51 | self.provisioningState = vmssmodel['properties']['provisioningState'] 52 | self.status = self.provisioningState 53 | 54 | # update the token property 55 | def update_token(self, access_token): 56 | self.access_token = access_token 57 | 58 | # update the VMSS model with any updated properties - extend this to include updatePolicy etc. 59 | def update_model(self, newversion, newvmsize): 60 | changes = 0 61 | if self.version != newversion: 62 | changes += 1 63 | if self.image_type == 'platform': 64 | self.model['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] = newversion 65 | else: 66 | self.model['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] = newversion 67 | if self.vmsize != newvmsize: 68 | changes += 1 69 | self.model['sku']['name'] = newvmsize # to do - add a check that the new vm size matches the tier 70 | self.version = newversion 71 | self.vmsize = newvmsize 72 | if changes == 0: 73 | self.status = 'VMSS model is unchanged, skipping update' 74 | else: 75 | # put the vmss model 76 | updateresult = azurerm.update_vmss(self.access_token, self.sub_id, self.rgname, self.name, 77 | json.dumps(self.model)) 78 | self.status = updateresult 79 | 80 | 81 | # set the VMSS to a new capacity 82 | def scale(self, capacity): 83 | self.model['sku']['capacity'] = capacity 84 | scaleoutput = azurerm.scale_vmss(self.access_token, self.sub_id, self.rgname, self.name, self.vmsize, self.tier, 85 | capacity) 86 | self.status = scaleoutput 87 | 88 | # power on all the VMs in the scale set 89 | def poweron(self): 90 | result = azurerm.start_vmss(self.access_token, self.sub_id, self.rgname, self.name) 91 | self.status = result 92 | 93 | def restart(self): 94 | result = azurerm.restart_vmss(self.access_token, self.sub_id, self.rgname, self.name) 95 | self.status = result 96 | 97 | # power off all the VMs in the scale set 98 | def poweroff(self): 99 | result = azurerm.poweroff_vmss(self.access_token, self.sub_id, self.rgname, self.name) 100 | self.status = result 101 | 102 | # stop deallocate all the VMs in the scale set 103 | def dealloc(self): 104 | result = azurerm.stopdealloc_vmss(self.access_token, self.sub_id, self.rgname, self.name) 105 | self.status = result 106 | 107 | # get the VMSS instance view and set the class property 108 | def init_vm_instance_view(self): 109 | # get an instance view list in order to build a heatmap 110 | self.vm_instance_view = \ 111 | azurerm.list_vmss_vm_instance_view(self.access_token, self.sub_id, self.rgname, self.name) 112 | 113 | # operations on individual VMs or groups of VMs in a scale set 114 | def reimagevm(self, vmstring): 115 | result = azurerm.reimage_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 116 | self.status = result 117 | 118 | def upgradevm(self, vmstring): 119 | result = azurerm.upgrade_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 120 | self.status = result 121 | 122 | def deletevm(self, vmstring): 123 | result = azurerm.delete_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 124 | self.status = result 125 | 126 | def startvm(self, vmstring): 127 | result = azurerm.start_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 128 | self.status = result 129 | 130 | def restartvm(self, vmstring): 131 | result = azurerm.restart_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 132 | self.status = result 133 | 134 | def deallocvm(self, vmstring): 135 | result = azurerm.stopdealloc_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 136 | self.status = result 137 | 138 | def poweroffvm(self, vmstring): 139 | result = azurerm.poweroff_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 140 | self.status = result 141 | 142 | def get_power_state(self, statuses): 143 | for status in statuses: 144 | if status['code'].startswith('Power'): 145 | return status['code'][11:] 146 | 147 | # create lists of VMs in the scale set by fault domain, update domain, and an all-up 148 | def set_domain_lists(self): 149 | self.fd_dict = {f: [] for f in range(5)} 150 | self.ud_dict = {u: [] for u in range(5)} 151 | self.vm_list = [] 152 | for instance in self.vm_instance_view['value']: 153 | try: 154 | instanceId = instance['instanceId'] 155 | ud = instance['properties']['instanceView']['platformUpdateDomain'] 156 | fd = instance['properties']['instanceView']['platformFaultDomain'] 157 | power_state = self.get_power_state(instance['properties']['instanceView']['statuses']) 158 | self.ud_dict[ud].append([instanceId, power_state]) 159 | self.fd_dict[fd].append([instanceId, power_state]) 160 | self.vm_list.append([instanceId, fd, ud, power_state]) 161 | except KeyError: 162 | print('KeyError - UD/FD may not be assigned yet. Instance view: ' + json.dumps(instance)) 163 | break 164 | 165 | -------------------------------------------------------------------------------- /vmssextn/vmss.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import azurerm 4 | 5 | 6 | class vmss(): 7 | def __init__(self, vmssname, vmssmodel, subscription_id, access_token): 8 | self.name = vmssname 9 | id = vmssmodel['id'] 10 | self.rgname = id[id.index('resourceGroups/') + 15:id.index('/providers')] 11 | self.sub_id = subscription_id 12 | self.access_token = access_token 13 | 14 | self.model = vmssmodel 15 | self.adminuser = vmssmodel['properties']['virtualMachineProfile']['osProfile']['adminUsername'] 16 | self.capacity = vmssmodel['sku']['capacity'] 17 | self.location = vmssmodel['location'] 18 | self.nameprefix = vmssmodel['properties']['virtualMachineProfile']['osProfile']['computerNamePrefix'] 19 | self.overprovision = vmssmodel['properties']['overprovision'] 20 | self.tier = vmssmodel['sku']['tier'] 21 | self.upgradepolicy = vmssmodel['properties']['upgradePolicy']['mode'] 22 | self.vmsize = vmssmodel['sku']['name'] 23 | 24 | # if it's a platform image, the model will have these 25 | if 'imageReference' in vmssmodel['properties']['virtualMachineProfile']['storageProfile']: 26 | self.image_type = 'platform' 27 | self.offer = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['offer'] 28 | self.sku = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['sku'] 29 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] 30 | # else it's a custom image it will have an image URI - to do: add something to display the image URI 31 | else: 32 | # for now just set these values so it doesn't break 33 | self.image_type = 'custom' 34 | self.offer = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['osType'] 35 | self.sku = 'custom' 36 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] 37 | 38 | self.provisioningState = vmssmodel['properties']['provisioningState'] 39 | self.status = self.provisioningState 40 | 41 | # update the model, useful to see if provisioning is complete 42 | def refresh_model(self): 43 | vmssmodel = azurerm.get_vmss(self.access_token, self.sub_id, self.rgname, self.name) 44 | self.model = vmssmodel 45 | self.capacity = vmssmodel['sku']['capacity'] 46 | self.vmsize = vmssmodel['sku']['name'] 47 | if self.image_type == 'platform': 48 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] 49 | else: 50 | self.version = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] 51 | self.provisioningState = vmssmodel['properties']['provisioningState'] 52 | self.status = self.provisioningState 53 | 54 | # update the token property 55 | def update_token(self, access_token): 56 | self.access_token = access_token 57 | 58 | # update the VMSS model with any updated properties - extend this to include updatePolicy etc. 59 | def update_model(self, newversion, newvmsize): 60 | changes = 0 61 | if self.version != newversion: 62 | changes += 1 63 | if self.image_type == 'platform': 64 | self.model['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] = newversion 65 | else: 66 | self.model['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] = newversion 67 | if self.vmsize != newvmsize: 68 | changes += 1 69 | self.model['sku']['name'] = newvmsize # to do - add a check that the new vm size matches the tier 70 | self.version = newversion 71 | self.vmsize = newvmsize 72 | if changes == 0: 73 | self.status = 'VMSS model is unchanged, skipping update' 74 | else: 75 | # put the vmss model 76 | updateresult = azurerm.update_vmss(self.access_token, self.sub_id, self.rgname, self.name, 77 | json.dumps(self.model)) 78 | self.status = updateresult 79 | 80 | # update the VMSS model with an updated extensionProfile 81 | def update_extns(self, extnprofile): 82 | self.model['properties']['virtualMachineProfile']['extensionProfile'] = extnprofile 83 | # put the vmss model 84 | updateresult = azurerm.update_vmss(self.access_token, self.sub_id, self.rgname, self.name, 85 | json.dumps(self.model)) 86 | self.status = updateresult 87 | 88 | # set the VMSS to a new capacity 89 | def scale(self, capacity): 90 | self.model['sku']['capacity'] = capacity 91 | scaleoutput = azurerm.scale_vmss(self.access_token, self.sub_id, self.rgname, self.name, self.vmsize, self.tier, 92 | capacity) 93 | self.status = scaleoutput 94 | 95 | # power on all the VMs in the scale set 96 | def poweron(self): 97 | result = azurerm.start_vmss(self.access_token, self.sub_id, self.rgname, self.name) 98 | self.status = result 99 | 100 | def restart(self): 101 | result = azurerm.restart_vmss(self.access_token, self.sub_id, self.rgname, self.name) 102 | self.status = result 103 | 104 | # power off all the VMs in the scale set 105 | def poweroff(self): 106 | result = azurerm.poweroff_vmss(self.access_token, self.sub_id, self.rgname, self.name) 107 | self.status = result 108 | 109 | # stop deallocate all the VMs in the scale set 110 | def dealloc(self): 111 | result = azurerm.stopdealloc_vmss(self.access_token, self.sub_id, self.rgname, self.name) 112 | self.status = result 113 | 114 | # get the VMSS instance view and set the class property 115 | def init_vm_instance_view(self): 116 | # get an instance view list in order to build a heatmap 117 | self.vm_instance_view = \ 118 | azurerm.list_vmss_vm_instance_view(self.access_token, self.sub_id, self.rgname, self.name) 119 | 120 | # operations on individual VMs or groups of VMs in a scale set 121 | def reimagevm(self, vmstring): 122 | result = azurerm.reimage_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 123 | self.status = result 124 | 125 | def upgradevm(self, vmstring): 126 | result = azurerm.upgrade_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 127 | self.status = result 128 | 129 | def deletevm(self, vmstring): 130 | result = azurerm.delete_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 131 | self.status = result 132 | 133 | def startvm(self, vmstring): 134 | result = azurerm.start_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 135 | self.status = result 136 | 137 | def restartvm(self, vmstring): 138 | result = azurerm.restart_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 139 | self.status = result 140 | 141 | def deallocvm(self, vmstring): 142 | result = azurerm.stopdealloc_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 143 | self.status = result 144 | 145 | def poweroffvm(self, vmstring): 146 | result = azurerm.poweroff_vmss_vms(self.access_token, self.sub_id, self.rgname, self.name, vmstring) 147 | self.status = result 148 | 149 | def get_power_state(self, statuses): 150 | for status in statuses: 151 | if status['code'].startswith('Power'): 152 | return status['code'][11:] 153 | 154 | # create lists of VMs in the scale set by fault domain, update domain, and an all-up 155 | def set_domain_lists(self): 156 | self.fd_dict = {f: [] for f in range(5)} 157 | self.ud_dict = {u: [] for u in range(5)} 158 | self.vm_list = [] 159 | for instance in self.vm_instance_view['value']: 160 | try: 161 | instanceId = instance['instanceId'] 162 | ud = instance['properties']['instanceView']['platformUpdateDomain'] 163 | fd = instance['properties']['instanceView']['platformFaultDomain'] 164 | power_state = self.get_power_state(instance['properties']['instanceView']['statuses']) 165 | self.ud_dict[ud].append([instanceId, power_state]) 166 | self.fd_dict[fd].append([instanceId, power_state]) 167 | self.vm_list.append([instanceId, fd, ud, power_state]) 168 | except KeyError: 169 | print('KeyError - UD/FD may not be assigned yet. Instance view: ' + json.dumps(instance)) 170 | break 171 | 172 | -------------------------------------------------------------------------------- /vmssupgrade/vmssupgrade.py: -------------------------------------------------------------------------------- 1 | # Python script to upgrade a VM Scale Set one update domain at a time 2 | # usage: vmssupgrade -r rgname -v vmssname -n newversion -u updatedomain [-y][--verbose][--nowait] 3 | # test values for ubuntu 14.04 platform image: 4 | # oldversion = "14.04.201506100" 5 | # newversion = "14.04.201507060" 6 | import argparse 7 | import json 8 | import sys 9 | import time 10 | 11 | import azurerm 12 | 13 | 14 | def get_vm_ids_by_ud(access_token, subscription_id, resource_group, vmssname, updatedomain): 15 | instanceviewlist = azurerm.list_vmss_vm_instance_view(access_token, subscription_id, resource_group, vmssname) 16 | # print(json.dumps(instanceviewlist, sort_keys=False, indent=2, separators=(',', ': '))) 17 | 18 | # loop through the instance view list, and build the vm id list of VMs in the matching UD 19 | udinstancelist = [] 20 | for instanceView in instanceviewlist['value']: 21 | vmud = instanceView['properties']['instanceView']['platformUpdateDomain'] 22 | if vmud == updatedomain: 23 | udinstancelist.append(instanceView['instanceId']) 24 | udinstancelist.sort() 25 | return udinstancelist 26 | 27 | 28 | def main(): 29 | # create parser 30 | argParser = argparse.ArgumentParser() 31 | 32 | argParser.add_argument('--vmssname', '-s', required=True, action='store', help='VM Scale Set name') 33 | argParser.add_argument('--resourcegroup', '-r', required=True, dest='resource_group', action='store', 34 | help='Resource group name') 35 | argParser.add_argument('--newversion', '-n', dest='newversion', action='store', 36 | help='New platform image version string') 37 | argParser.add_argument('--customuri', '-c', dest='customuri', action='store', help='New custom image URI string') 38 | argParser.add_argument('--updatedomain', '-u', dest='updatedomain', action='store', type=int, 39 | help='Update domain (int)') 40 | argParser.add_argument('--vmid', '-i', dest='vmid', action='store', type=int, help='Single VM ID (int)') 41 | argParser.add_argument('--vmlist', '-l', dest='vmlist', action='store', help='List of VM IDs e.g. "["1", "2"]"') 42 | argParser.add_argument('--nowait', '-w', action='store_true', default=False, 43 | help='Start upgrades and then exit without waiting') 44 | argParser.add_argument('--verbose', '-v', action='store_true', default=False, help='Show additional information') 45 | argParser.add_argument('-y', dest='noprompt', action='store_true', default=False, 46 | help='Do not prompt for confirmation') 47 | 48 | args = argParser.parse_args() 49 | 50 | # switches to determine program behavior 51 | noprompt = args.noprompt # go ahead and upgrade without waiting for confirmation when True 52 | nowait = args.nowait # don't loop waiting for upgrade provisioning to complete when True 53 | verbose = args.verbose # print extra status information when True 54 | 55 | vmssname = args.vmssname 56 | resource_group = args.resource_group 57 | if args.newversion is not None: 58 | newversion = args.newversion 59 | storagemode = 'platform' 60 | elif args.customuri is not None: 61 | customuri = args.customuri 62 | storagemode = 'custom' 63 | else: 64 | argParser.error('You must specify a new version for platform images or a custom uri for custom images') 65 | 66 | if args.updatedomain is not None: 67 | updatedomain = args.updatedomain 68 | upgrademode = 'updatedomain' 69 | elif args.vmid is not None: 70 | vmid = args.vmid 71 | upgrademode = 'vmid' 72 | elif args.vmlist is not None: 73 | vmlist = args.vmlist 74 | upgrademode = 'vmlist' 75 | else: 76 | argParser.error('You must specify an update domain, a vm id, or a vm list') 77 | 78 | # Load Azure app defaults 79 | try: 80 | with open('vmssconfig.json') as configFile: 81 | configdata = json.load(configFile) 82 | except FileNotFoundError: 83 | print("Error: Expecting vmssconfig.json in current folder") 84 | sys.exit() 85 | 86 | tenant_id = configdata['tenantId'] 87 | app_id = configdata['appId'] 88 | app_secret = configdata['appSecret'] 89 | subscription_id = configdata['subscriptionId'] 90 | 91 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 92 | 93 | # get the vmss model 94 | vmssmodel = azurerm.get_vmss(access_token, subscription_id, resource_group, vmssname) 95 | # print(json.dumps(vmssmodel, sort_keys=False, indent=2, separators=(',', ': '))) 96 | 97 | if storagemode == 'platform': 98 | # check current version 99 | imagereference = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference'] 100 | print('Current image reference in Scale Set model:') 101 | print(json.dumps(imagereference, sort_keys=False, indent=2, separators=(',', ': '))) 102 | 103 | # compare current version with new version 104 | if imagereference['version'] == newversion: 105 | print('Scale Set model version is already set to ' + newversion + ', skipping model update.') 106 | else: 107 | if not noprompt: 108 | response = input('Confirm version upgrade to: ' + newversion + ' (y/n)') 109 | if response.lower() != 'y': 110 | sys.exit(1) 111 | # change the version 112 | vmssmodel['properties']['virtualMachineProfile']['storageProfile']['imageReference']['version'] = newversion 113 | # put the vmss model 114 | updateresult = azurerm.update_vmss(access_token, subscription_id, resource_group, vmssname, 115 | json.dumps(vmssmodel)) 116 | if verbose: 117 | print(updateresult) 118 | print('OS version updated to ' + newversion + ' in model for VM Scale Set: ' + vmssname) 119 | else: # storagemode = custom 120 | # check current uri 121 | oldimageuri = vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] 122 | print('Current image URI in Scale Set model:' + oldimageuri) 123 | 124 | # compare current uri with new uri 125 | if oldimageuri == customuri: 126 | print('Scale Set model version is already set to ' + customuri + ', skipping model update.') 127 | else: 128 | if not noprompt: 129 | response = input('Confirm uri upgrade to: ' + customuri + ' (y/n)') 130 | if response.lower() != 'y': 131 | sys.exit(1) 132 | # change the version 133 | vmssmodel['properties']['virtualMachineProfile']['storageProfile']['osDisk']['image']['uri'] = customuri 134 | # put the vmss model 135 | updateresult = azurerm.update_vmss(access_token, subscription_id, resource_group, vmssname, 136 | json.dumps(vmssmodel)) 137 | if verbose: 138 | print(updateresult) 139 | print('Image URI updated to ' + customuri + ' in model for VM Scale Set: ' + vmssname) 140 | 141 | # build the list of VMs to upgrade depending on the upgrademode setting 142 | if upgrademode == 'updatedomain': 143 | # list the VMSS VM instance views to determine their update domains 144 | print('Examining the scale set..') 145 | udinstancelist = get_vm_ids_by_ud(access_token, subscription_id, resource_group, vmssname, updatedomain) 146 | print('VM instances in UD: ' + str(updatedomain) + ' to upgrade:') 147 | print(udinstancelist) 148 | vmids = json.dumps(udinstancelist) 149 | print('Upgrading VMs in UD: ' + str(updatedomain)) 150 | elif upgrademode == 'vmid': 151 | vmids = json.dumps([str(vmid)]) 152 | print('Upgrading VM ID: ' + str(vmid)) 153 | else: # upgrademode = vmlist 154 | vmids = vmlist 155 | print('Upgrading VM IDs: ' + vmlist) 156 | 157 | # do manualupgrade on the VMs in the list 158 | upgraderesult = azurerm.upgrade_vmss_vms(access_token, subscription_id, resource_group, vmssname, vmids) 159 | print(upgraderesult) 160 | 161 | # now wait for upgrade to complete 162 | # query VM scale set instance view 163 | if not nowait: 164 | updatecomplete = False 165 | provisioningstate = '' 166 | while not updatecomplete: 167 | vmssinstanceview = azurerm.get_vmss_instance_view(access_token, subscription_id, resource_group, vmssname) 168 | for status in vmssinstanceview['statuses']: 169 | provisioningstate = status['code'] 170 | if provisioningstate == 'ProvisioningState/succeeded': 171 | updatecomplete = True 172 | if verbose: 173 | print(provisioningstate) 174 | time.sleep(5) 175 | print(status['code']) 176 | else: 177 | print('Check Scale Set provisioning state to determine when upgrade is complete.') 178 | 179 | 180 | if __name__ == "__main__": 181 | main() 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vmsstools 2 | Azure VM Scale Set tools. Use at your own risk :-) 3 | 4 | ## jumpbox 5 | 6 | Creates a new VM in a resource group. You can use this to create a VM in an empty resource group if you want, or use it to create a jump box in an existing VNET to be used to connect to existing resources in the VNET like VM scale set VMs. 7 | 8 | ``` 9 | usage: jumpbox.py [-h] --vmname VMNAME --rgname RGNAME [--user USER] 10 | [--password PASSWORD] [--sshkey SSHKEY] [--sshpath SSHPATH] 11 | [--location LOCATION] [--dns DNS] [--vnet VNET] [--nowait] 12 | [--nonsg] [--verbose] 13 | 14 | The following arguments are required: --vmname/-n, --rgname/-g 15 | ``` 16 | 17 | This tool will create a VM in the first VNET it finds in the resource group (or the VNET you specify). If it doesn't find a VNET it will create one. 18 | 19 | You can provide a user and password/public key/public key file. If no authentication method provided and no default public key file found it will create a password for you. 20 | 21 | By default jumpbox.py will wait for the VM to be provisioned, unless you specify the --nowait argument. 22 | 23 | ![jumpbox screenshot](./docs/jumpbox.png) 24 | 25 | ## vipswap 26 | 27 | Swaps the public IP addresses between two Azure load balancers. 28 | 29 | ``` 30 | usage: vip_swap.py [-h] --resourcegroup RESOURCE_GROUP --lb1 LB1 --lb2 LB2 31 | [--verbose] [-y] 32 | The following arguments are required: --resourcegroup/-g, --lb1/-1, --lb2/-2 33 | ``` 34 | 35 | ![vipswap screenshot](./docs/vipswap.png) 36 | 37 | 38 | ## cpuload - Random load generator for Azure VM scale set VMs 39 | A set of scripts to trigger a randmon CPU load against the VMs in a scale set. Tested up to 1000 VMs. 40 | 41 | To generate a load: 42 | 1. Deploy a VM scale set which includes a custom script extension which deploys a Python bottle server: [https://github.com/gbowerman/azure-myriad/blob/master/bigtest/bigbottle.json](https://github.com/gbowerman/azure-myriad/blob/master/bigtest/bigbottle.json) 43 | 2. Deploy a jumpbox VM in the same subnet. E.g. use [https://github.com/gbowerman/vmsstools/tree/master/jumpbox](https://github.com/gbowerman/vmsstools/tree/master/jumpbox) 44 | 3. ssh to the jumpbox VM and use the getnics.py script to generate a list of VMSS VMs. Note: this requires an _azurermconfig.json_ file in the same directory which contains your service principal app/tenant details and subscription id. 45 | ```bash 46 | python3 getnics.py > ipaddrs 47 | ``` 48 | 4. Edit random-load.py to set your resource group name and scale set name. 49 | 5. start a load running in background with: 50 | ```bash 51 | nohup python3 random-load.py > random.out 2>&1 & 52 | ``` 53 | 6. kill the process when you're done. 54 | 55 | ## vmss_cpu_plot 56 | 57 | Shows CPU usage graph for a VM scale set. 58 | 59 | ![CPU graph screenshot](./docs/cpu_graph.png) 60 | 61 | Usage: vmss_cpu_plot.py [-h] --vmss VMSS --resourcegroup RESOURCE_GROUP [--verbose] 62 | 63 | Graphs CPU usage for the named scale set for the last hour. 64 | 65 | 66 | ## vmssvmname 67 | 68 | Converts a VM scale set VM hostname into a VM id. E.g. 69 | 70 | ``` 71 | > python vmssvmname.py --vmid 1146 --prefix myvmprefix 72 | hostname = myvmprefix0000VU 73 | 74 | > python vmssvmname.py --hostname myvmprefix0000VU 75 | VM ID = 1146 76 | ``` 77 | 78 | ## vmssextn 79 | 80 | Tool to read, add, remove extensions from an Azure VM Scale Set. 81 | 82 | ![vmssextn screenshot](./docs/vmssextn_show.png) 83 | 84 | This script can: 85 | 86 | 1. Show the extensions which are installed in a Virtual Machine Scale Set. 87 | 2. Delete an extension from a VM Scale Set by name. 88 | 3. Add an extension to a VM Scale Set by providing a JSON file containing the properties. 89 | 90 | Note: this tool just updates the VMSS model. After that, if the VMSS 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 91 | 92 | ### Showing which extensions are installed 93 | 94 | Lists the extensions installed in a specified scale set, including name, publisher, version. 95 | 96 | Use –verbose to dump the JSON properties, which could then be saved to a file and used to add extensions to other scale sets. 97 | 98 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest 99 | 100 | ### Adding 101 | 102 | Add an extension to a VM Scale Set by specifying a file containing an extension definition in JSON format. 103 | 104 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --add extnfile 105 | 106 | For an example JSON extension definition file to add, see: [lapextension.json](./lapextension.json) 107 | 108 | Note: this tool just updates the VMSS model. After that, if the VMSS the 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 109 | 110 | ### Deleting an extension 111 | 112 | Delete a VM extension from a VM Scale Set by name. 113 | 114 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --delete extnname 115 | 116 | Note: this tool just updates the VMSS model. After that, if the VMSS 'upgradePolicy' property is set to 'Manual', you will have to call manualUgrade on the VMs to apply the update. 117 | 118 | ### Updating an extension 119 | 120 | Updates a VM extension from a VM Scale Set by name using a JSON definition in a file. 121 | 122 | E.g. python vmssextn.py --resourcegroup extntest --vmssname extntest --update extnfile 123 | 124 | 125 | ## vmssupgrade 126 | 127 | Tool to roll out an OS upgrade to a running VM Scale Set, one upgrade domain at a time. 128 | 129 | This script performs the following steps: 130 | 131 | 1. Query the VM Scale Set model and check the current OS version. 132 | 2. Confirms OS upgrade with the user (unless --noprompt is used). 133 | 3. Updates the VMSS model to the new version. At this point no VMs have been upgraded, unless the VMSS _upgradePolicy_ property is set to "Automatic"). 134 | 4. Does a _manualUpgrade_ on the VMs in the selected update domain (or specific VMs, depending on the command line parameters). 135 | 5. Waits until the VMSS provisioning state has finished updating (unless the --nowait command line parameter is used). 136 | 137 | ![vmssupgrade screenshot](./docs/vmssupgrade-screenshot.png) 138 | 139 | ### Installation 140 | 1. Install Python 3.x. 141 | 2. Install the azurerm REST wrappers for Microsoft Azure: "pip install azurerm" (use --upgrade if azurerm is already installed) 142 | 3. Clone this repo locally. In particular copy the vmssupgrade folder. 143 | 4. You need a service principal and tenant ID. See [Authenticating a service principal with Azure Resource Manager](https://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/) - note that "Reader" access as described in that doc is not enough. It should be "Contributor" or some other roll that allows write access. 144 | 6. Edit vmssconfig.json in the local directory (rename vmssconfig.json.tmpl). Fill in the service principal values for your application (tenantId, appId, app secret, subscription ID). 145 | 7. usage: vmssupgrade -r rgname -s vmssname -n newversion {-u updatedomain|-i vmid|-l vmlist} [-y][-v][-h] 146 | 147 | ### Examples 148 | 149 | **Upgrading a single VM in a VM Scale Set** 150 | 151 | This example upgrades VM id #1 and sets verbose mode, which will display the provisioning state every 5 seconds until complete.. 152 | 153 | vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201506100" -i 1 -v 154 | 155 | **Upgrading upgrade domain 0 of a VM Scale Set** 156 | 157 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --updatedomain 0 158 | 159 | **Upgrading a custom image URI in upgrade domain 0 of a VM Scale Set** 160 | 161 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --customuri "path_to_custom_image" --updatedomain 0 162 | 163 | **Start an upgrade but don't wait for it to complete** 164 | 165 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --updatedomain 0 --nowait 166 | 167 | **Upgrade an arbitrary list of VMs in a VM Scale Set** 168 | 169 | python vmssupgrade.py --resourcegroup myrg --vmssname myvmss --newversion "14.04.201507060" --vmlist '["1","2","3","4"]' 170 | 171 | **Quick video walkthrough** 172 | 173 | [![vmssupgrade demo](https://img.youtube.com/vi/0sc9YMgvXLY/0.jpg)](https://www.youtube.com/watch?v=0sc9YMgvXLY) 174 | 175 | ### Notes 176 | 177 | - The ability to update an OS version in a running scale set was only recently enabled in production. Your account may need to be whitelisted if you try this before April 23 2016. Contact @gbowerman for more details. 178 | 179 | - This script is light on error checking and should be thought of as a proof of concept. If you're thinking of using it in production you'd need to do some work on it to improve error handling and logging. 180 | 181 | - This script is designed to work with both platform images (updates the _version_ property of the _imageReference_) and custom images (updating the _osDisk->image->Uri_), though custom image support hasn't been tested yet. Let me know if it works :-) 182 | 183 | - You can update the version of a platform image. Updating the _sku_ (for example going from Ubuntu 15.10 to 16.04) is not possible. This maybe possible for VM Scale Sets in the future. 184 | -------------------------------------------------------------------------------- /jumpbox/jumpbox.py: -------------------------------------------------------------------------------- 1 | # program to create a jumpbox VM in an existing VNET for diagnostic purposes, e.g. ssh to local VMs 2 | # uses Managed Disks, assumes first subnet in VNET has room, uses CoreOs (change code if needed) 3 | # Arguments: 4 | # --vmname [resource names are defaulted from this] 5 | # --rgname 6 | # --user [optional, defaults to azure] 7 | # --location [optional, defaults to VNET location] 8 | # --vnet [optional vnet name, otherwise first vnet in resource group is picked] 9 | # --dns [optional unique DNS name, otherwise uses vmname + 'dns' 10 | # --password/--sshkey/--sshpath [optional pick an authentication method, 11 | # otherwise if no .ssh/id_rsa.pub found will create a password for you] 12 | # --nowait [optional to not wait for VM to be successfully provisioned] 13 | # --nosng [optional to not create an nsg on the VM (default is to only open port 22)] 14 | import argparse 15 | import azurerm 16 | import json 17 | from haikunator import Haikunator 18 | import os 19 | from os.path import expanduser 20 | import sys 21 | import time 22 | 23 | # validate command line arguments 24 | argParser = argparse.ArgumentParser() 25 | 26 | argParser.add_argument('--vmname', '-n', required=True, action='store', help='Name') 27 | argParser.add_argument('--rgname', '-g', required=True, action='store', help='Resource Group Name') 28 | argParser.add_argument('--user', '-u', required=False, action='store', default='azure', help='Optional username') 29 | argParser.add_argument('--password', '-p', required=False, action='store', help='Optional password') 30 | argParser.add_argument('--sshkey', '-k', required=False, action='store', help='SSH public key') 31 | argParser.add_argument('--sshpath', '-s', required=False, action='store', help='SSH public key file path') 32 | argParser.add_argument('--location', '-l', required=False, action='store', help='Location, e.g. eastus') 33 | argParser.add_argument('--vmsize', required=False, action='store', default='Standard_D1_V2', help='VM size, defaults to Standard_D1_V2') 34 | argParser.add_argument('--dns', '-d', required=False, action='store', help='DNS, e.g. myuniquename') 35 | argParser.add_argument('--vnet', required=False, action='store', help='Optional VNET Name (otherwise first VNET in resource group is used)') 36 | argParser.add_argument('--nowait', action='store_true', default=False, help='Do not wait for VM to finish provisioning') 37 | argParser.add_argument('--nonsg', action='store_true', default=False, help='Do not create a network security group on the NIC') 38 | argParser.add_argument('--verbose', '-v', action='store_true', default=False, help='Print operational details') 39 | 40 | args = argParser.parse_args() 41 | 42 | name = args.vmname 43 | rgname = args.rgname 44 | vnet = args.vnet 45 | location = args.location 46 | username = args.user 47 | password = args.password 48 | sshkey = args.sshkey 49 | sshpath = args.sshpath 50 | verbose = args.verbose 51 | dns_label = args.dns 52 | no_wait = args.nowait 53 | no_nsg = args.nonsg 54 | vmsize = args.vmsize 55 | 56 | # make sure all authentication scenarios are handled 57 | if sshkey is not None and sshpath is not None: 58 | sys.exit('Error: You can provide an SSH public key, or a public key file path, not both.') 59 | if password is not None and (sshkey is not None or sshpath is not None): 60 | sys.exit('Error: provide a password or SSH key (or nothing), not both') 61 | 62 | use_password = False 63 | if password is not None: 64 | use_password = True 65 | else: 66 | if sshkey is None and sshpath is None: # no auth parameters were provided 67 | # look for ~/id_rsa.pub 68 | home = expanduser('~') 69 | sshpath = home + os.sep + '.ssh' + os.sep + 'id_rsa.pub' 70 | if os.path.isfile(sshpath) is False: 71 | print('Default public key file not found.') 72 | use_password = True 73 | password = Haikunator().haikunate(delimiter=',') # creates random password 74 | print('Created new password = ' + password) 75 | else: 76 | print('Default public key file found') 77 | 78 | if use_password is False: 79 | print('Reading public key..') 80 | if sshkey is None: 81 | # at this point sshpath should have a valid Value 82 | with open(sshpath, 'r') as pub_ssh_file_fd: 83 | sshkey = pub_ssh_file_fd.read() 84 | 85 | # Load Azure app defaults 86 | try: 87 | with open('azurermconfig.json') as configFile: 88 | configData = json.load(configFile) 89 | except FileNotFoundError: 90 | print("Error: Expecting azurermconfig.json in current folder") 91 | sys.exit() 92 | 93 | tenant_id = configData['tenantId'] 94 | app_id = configData['appId'] 95 | app_secret = configData['appSecret'] 96 | subscription_id = configData['subscriptionId'] 97 | 98 | # authenticate 99 | access_token = azurerm.get_access_token(tenant_id, app_id, app_secret) 100 | 101 | # if no location parameter was specified now would be a good time to figure out the location 102 | if location is None: 103 | try: 104 | rg = azurerm.get_resource_group(access_token, subscription_id, rgname) 105 | location = rg['location'] 106 | except KeyError: 107 | print('Cannot find resource group ' + rgname + '. Check connection/authorization.') 108 | print(json.dumps(rg, sort_keys=False, indent=2, separators=(',', ': '))) 109 | sys.exit() 110 | print('location = ' + location) 111 | 112 | # get VNET 113 | print('Getting VNet') 114 | vnet_not_found = False 115 | if vnet is None: 116 | print('VNet not set, checking resource group') 117 | # get first VNET in resource group 118 | try: 119 | vnets = azurerm.list_vnets_rg(access_token, subscription_id, rgname) 120 | # print(json.dumps(vnets, sort_keys=False, indent=2, separators=(',', ': '))) 121 | vnetresource = vnets['value'][0] 122 | except IndexError: 123 | print('No VNET found in resource group.') 124 | vnet_not_found = True 125 | vnet = name + 'vnet' 126 | else: 127 | print('Getting VNet: ' + vnet) 128 | vnetresource = azurerm.get_vnet(access_token, subscription_id, rgname, vnet) 129 | if 'properties' not in vnetresource: 130 | print('VNet ' + vnet + ' not found in resource group ' + rgname) 131 | vnet_not_found = True 132 | 133 | if vnet_not_found is True: 134 | # create a vnet 135 | print('Creating vnet: ' + vnet) 136 | rmresource = azurerm.create_vnet(access_token, subscription_id, rgname, vnet, location, \ 137 | address_prefix='10.0.0.0/16', nsg_id=None) 138 | if rmresource.status_code != 201: 139 | print('Error ' + str(vnetresource.status_code) + ' creating VNET. ' + vnetresource.text) 140 | sys.exit() 141 | vnetresource = azurerm.get_vnet(access_token, subscription_id, rgname, vnet) 142 | try: 143 | subnet_id = vnetresource['properties']['subnets'][0]['id'] 144 | except KeyError: 145 | print('Subnet not found for VNet ' + vnet) 146 | sys.exit() 147 | if verbose is True: 148 | print('subnet_id = ' + subnet_id) 149 | 150 | public_ip_name = name + 'ip' 151 | if dns_label is None: 152 | dns_label = name + 'dns' 153 | 154 | print('Creating public ipaddr') 155 | rmreturn = azurerm.create_public_ip(access_token, subscription_id, rgname, public_ip_name, dns_label, location) 156 | if rmreturn.status_code not in [200,201]: 157 | print(rmreturn.text) 158 | sys.exit('Error: ' + str(rmreturn.status_code) + ' from azurerm.create_public_ip()') 159 | ip_id = rmreturn.json()['id'] 160 | if verbose is True: 161 | print('ip_id = ' + ip_id) 162 | 163 | print('Waiting for IP provisioning..') 164 | waiting = True 165 | while waiting: 166 | ip = azurerm.get_public_ip(access_token, subscription_id, rgname, public_ip_name) 167 | if ip['properties']['provisioningState'] == 'Succeeded': 168 | waiting = False 169 | time.sleep(1) 170 | 171 | if no_nsg is True: 172 | nsg_id = None 173 | else: 174 | # create NSG 175 | nsg_name = name + 'nsg' 176 | print('Creating NSG: ' + nsg_name) 177 | rmreturn = azurerm.create_nsg(access_token, subscription_id, rgname, nsg_name, location) 178 | if rmreturn.status_code != 201: 179 | print('Error ' + str(rmreturn.status_code) + ' creating NSG. ' + rmreturn.text) 180 | sys.exit() 181 | nsg_id = rmreturn.json()['id'] 182 | 183 | # create NSG rule for ssh, scp 184 | nsg_rule = 'ssh' 185 | print('Creating NSG rule: ' + nsg_rule) 186 | rmreturn = azurerm.create_nsg_rule(access_token, subscription_id, rgname, nsg_name, nsg_rule, \ 187 | description='ssh rule', destination_range='22') 188 | if rmreturn.status_code != 201: 189 | print('Error ' + str(rmreturn.status_code) + ' creating NSG rule. ' + rmreturn.text) 190 | sys.exit() 191 | 192 | # create NIC 193 | nic_name = name + 'nic' 194 | print('Creating NIC: ' + nic_name) 195 | rmreturn = azurerm.create_nic(access_token, subscription_id, rgname, nic_name, ip_id, subnet_id, \ 196 | location, nsg_id=nsg_id) 197 | if rmreturn.status_code != 201: 198 | print('Error ' + rmreturn.status_code + ' creating NSG rule. ' + rmreturn.text) 199 | sys.exit() 200 | nic_id = rmreturn.json()['id'] 201 | 202 | print('Waiting for NIC provisioning..') 203 | waiting = True 204 | while waiting: 205 | nic = azurerm.get_nic(access_token, subscription_id, rgname, nic_name) 206 | if nic['properties']['provisioningState'] == 'Succeeded': 207 | waiting = False 208 | time.sleep(1) 209 | 210 | # create VM 211 | vm_name = name 212 | #publisher = 'CoreOS' 213 | #offer = 'CoreOS' 214 | #sku = 'Stable' 215 | publisher = 'Canonical' 216 | offer = 'UbuntuServer' 217 | sku = '16.04-LTS' 218 | version = 'latest' 219 | 220 | print('Creating VM: ' + vm_name) 221 | if use_password == True: 222 | rmreturn = azurerm.create_vm(access_token, subscription_id, rgname, vm_name, vmsize, publisher, offer, sku, 223 | version, nic_id, location, username=username, password=password) 224 | else: 225 | rmreturn = azurerm.create_vm(access_token, subscription_id, rgname, vm_name, vmsize, publisher, offer, sku, 226 | version, nic_id, location, username=username, public_key = sshkey) 227 | if rmreturn.status_code != 201: 228 | print('Error ' + rmreturn.status_code + ' creating VM. ' + rmreturn.text) 229 | sys.exit() 230 | if no_wait == False: 231 | print('Waiting for VM provisioning..') 232 | waiting = True 233 | while waiting: 234 | vm = azurerm.get_vm(access_token, subscription_id, rgname, vm_name) 235 | if vm['properties']['provisioningState'] == 'Succeeded': 236 | waiting = False 237 | time.sleep(5) 238 | print('VM provisioning complete.') 239 | print('Connect with:') 240 | print('ssh ' + dns_label + '.' + location + '.cloudapp.azure.com -l ' + username) 241 | -------------------------------------------------------------------------------- /rollingupgrade/rollingupgrade.py: -------------------------------------------------------------------------------- 1 | # VMSS Editor - Azure VM Scale Set management tool 2 | # vmsseditor.py - GUI component of Rolling Upgrade, tkinter based 3 | # - uses vmss.py and subscription.py classes for Azure operations 4 | """ 5 | Copyright (c) 2016, Guy Bowerman 6 | Description: Graphical dashboard to show and set Azure VM Scale Set properties 7 | License: MIT (see LICENSE.txt file for details) 8 | """ 9 | 10 | import json 11 | import os 12 | import sys 13 | import threading 14 | import time 15 | import tkinter as tk 16 | from tkinter import messagebox 17 | import subscription 18 | import vmss 19 | 20 | # size and color defaults 21 | btnwidth = 14 22 | entrywidth = 15 23 | if os.name == 'mac': 24 | geometry1 = '740x328' 25 | geometry2 = '740x640' 26 | else: 27 | geometry1 = '540x128' 28 | geometry2 = '540x440' 29 | frame_bgcolor = '#B0E0E6' 30 | canvas_bgcolor = '#F0FFFF' 31 | btncolor = '#F8F8FF' 32 | 33 | # Load Azure app defaults 34 | try: 35 | with open('vmssconfig.json') as configFile: 36 | configData = json.load(configFile) 37 | except FileNotFoundError: 38 | print("Error: Expecting vmssconfig.json in current folder") 39 | sys.exit() 40 | 41 | sub = subscription.subscription(configData['tenantId'], configData['appId'], configData['appSecret'], 42 | configData['subscriptionId']) 43 | current_vmss = None 44 | refresh_thread_running = False 45 | 46 | # thread to keep access token alive 47 | def subidkeepalive(): 48 | while True: 49 | time.sleep(2000) 50 | sub.auth() 51 | current_vmss.update_token(sub.access_token) 52 | 53 | # thread to refresh details until provisioning is complete 54 | def refresh_loop(): 55 | global refresh_thread_running 56 | while True: 57 | while (refresh_thread_running == True): 58 | current_vmss.refresh_model() 59 | if current_vmss.status == 'Succeeded' or current_vmss.status == 'Failed': 60 | refresh_thread_running = False 61 | time.sleep(10) 62 | vmssdetails() 63 | time.sleep(10) 64 | 65 | # rolling upgrade thread 66 | def rolling_upgrade_engine(batchsize, pausetime, vmbyfd_list): 67 | global refresh_thread_running 68 | # loop through all VMs 69 | num_vms_to_upgrade = len(vmbyfd_list) 70 | upgrade_index = 0 # running count of VMs updated or in batch to update 71 | while upgrade_index < num_vms_to_upgrade: 72 | # determine the next batch of VM IDs 73 | batch_list = [] 74 | for batch_index in range(batchsize): 75 | batch_list.append(vmbyfd_list[upgrade_index][0]) 76 | upgrade_index += 1 77 | if upgrade_index == num_vms_to_upgrade: 78 | break 79 | 80 | # do an upgrade on the batch 81 | print('Upgrading batch') 82 | current_vmss.upgradevm(json.dumps(batch_list)) 83 | statusmsg(current_vmss.status) 84 | refresh_thread_running = True 85 | 86 | # wait for upgrade to complete 87 | print('Starting refresh wait') 88 | while (refresh_thread_running == True): 89 | time.sleep(1) 90 | print('Batch complete') 91 | # wait for pausetime 92 | time.sleep(pausetime) 93 | 94 | # start timer thread 95 | timer_thread = threading.Thread(target=subidkeepalive, args=()) 96 | timer_thread.daemon = True 97 | timer_thread.start() 98 | 99 | # start refresh thread 100 | refresh_thread = threading.Thread(target=refresh_loop, args=()) 101 | refresh_thread.daemon = True 102 | refresh_thread.start() 103 | 104 | 105 | def assign_color_to_power_state(powerstate): 106 | if powerstate == 'running': 107 | return 'green' 108 | elif powerstate == 'stopped': 109 | return 'red' 110 | elif powerstate == 'starting': 111 | return 'yellow' 112 | elif powerstate == 'stopping': 113 | return 'orange' 114 | elif powerstate == 'deallocating': 115 | return 'grey' 116 | elif powerstate == 'deallocated': 117 | return 'black' 118 | else: # unknown 119 | return 'blue' 120 | 121 | # draw a grid to delineate fault domains and update domains on the VMSS heatmap 122 | def draw_grid(): 123 | vmcanvas.delete("all") 124 | # horizontal lines for UDs 125 | for y in range(4): 126 | ydelta = y * 35 127 | vmcanvas.create_text(15, ydelta + 30, text='UD ' + str(y)) 128 | vmcanvas.create_line(35, 50 + ydelta, 520, 50 + ydelta) 129 | vmcanvas.create_text(15, 170, text='UD 4') 130 | 131 | # vertical lines for FDs 132 | for x in range(4): 133 | xdelta = x * 100 134 | vmcanvas.create_text(45 + xdelta, 10, text='FD ' + str(x)) 135 | vmcanvas.create_line(132 + xdelta, 20, 132 + xdelta, 180, dash=(4, 2)) 136 | vmcanvas.create_text(445, 10, text='FD 4') 137 | 138 | # draw a heat map for the VMSS VMs - uses the set_domain_lists() function from the vmss class 139 | def draw_vms(vmssinstances): 140 | xval = 35 141 | yval = 20 142 | diameter = 15 143 | draw_grid() 144 | # current_vmss.clear_domain_lists() 145 | current_vmss.set_domain_lists() 146 | matrix = [[0 for x in range(5)] for y in range(5)] 147 | for vm in current_vmss.vm_list: 148 | instance_id = vm[0] 149 | fd = vm[1] 150 | ud = vm[2] 151 | powerstate = vm[3] 152 | statuscolor = assign_color_to_power_state(powerstate) 153 | xdelta = (fd * 100) + (matrix[ud][fd] * 20) 154 | ydelta = ud * 35 155 | # colored circle represents machine power state 156 | vmcanvas.create_oval(xval + xdelta, yval + ydelta, xval + xdelta + diameter, yval + ydelta + diameter, fill=statuscolor) 157 | # print VM ID under each circle 158 | vmcanvas.create_text(xval + xdelta + 7, yval + ydelta + 22, text=instance_id) 159 | matrix[ud][fd] += 1 160 | 161 | 162 | def getfds(): 163 | fd = int(selectedfd.get()) 164 | fdinstancelist = [] 165 | # print(json.dumps(current_vmss.fd_dict)) 166 | for entry in current_vmss.fd_dict[fd]: 167 | fdinstancelist.append(entry[0]) # entry[0] is the instance id 168 | # build list of UDs 169 | return fdinstancelist 170 | 171 | 172 | def startfd(): 173 | global refresh_thread_running 174 | fdinstancelist = getfds() 175 | current_vmss.startvm(json.dumps(fdinstancelist)) 176 | statusmsg(current_vmss.status) 177 | refresh_thread_running = True 178 | 179 | 180 | def powerfd(): 181 | global refresh_thread_running 182 | fdinstancelist = getfds() 183 | current_vmss.poweroffvm(json.dumps(fdinstancelist)) 184 | statusmsg(current_vmss.status) 185 | refresh_thread_running = True 186 | 187 | 188 | def reimagefd(): 189 | global refresh_thread_running 190 | fdinstancelist = getfds() 191 | current_vmss.reimagevm(json.dumps(fdinstancelist)) 192 | statusmsg(current_vmss.status) 193 | refresh_thread_running = True 194 | 195 | 196 | def upgradefd(): 197 | global refresh_thread_running 198 | fdinstancelist = getfds() 199 | current_vmss.upgradevm(json.dumps(fdinstancelist)) 200 | statusmsg(current_vmss.status) 201 | refresh_thread_running = True 202 | 203 | 204 | def rollingupgrade(): 205 | batchsize = int(batchtext.get()) 206 | pausetime = int(pausetext.get()) 207 | 208 | # get list of VMs ordered by FD - get this by concatenating the vmss fd_dict into a single list 209 | vmbyfd_list = [] 210 | for fdval in range(5): 211 | vmbyfd_list += current_vmss.fd_dict[fdval] 212 | num_vms_to_upgrade = len(vmbyfd_list) # should be the same of vmss capacity if starting in consistent state 213 | 214 | # launch rolling update thread 215 | rolling_upgrade_thread = threading.Thread(target=rolling_upgrade_engine, args=(batchsize, pausetime, vmbyfd_list,)) 216 | rolling_upgrade_thread.daemon = True 217 | rolling_upgrade_thread.start() 218 | 219 | 220 | def reimagevm(): 221 | global refresh_thread_running 222 | vmid = vmtext.get() 223 | vmstring = '["' + vmid + '"]' 224 | current_vmss.reimagevm(vmstring) 225 | statusmsg(current_vmss.status) 226 | refresh_thread_running = True 227 | 228 | 229 | def upgradevm(): 230 | global refresh_thread_running 231 | vmid = vmtext.get() 232 | vmstring = '["' + vmid + '"]' 233 | current_vmss.upgradevm(vmstring) 234 | statusmsg(current_vmss.status) 235 | refresh_thread_running = True 236 | 237 | 238 | def deletevm(): 239 | global refresh_thread_running 240 | vmid = vmtext.get() 241 | vmstring = '["' + vmid + '"]' 242 | current_vmss.deletevm(vmstring) 243 | statusmsg(current_vmss.status) 244 | refresh_thread_running = True 245 | 246 | 247 | def startvm(): 248 | global refresh_thread_running 249 | vmid = vmtext.get() 250 | vmstring = '["' + vmid + '"]' 251 | current_vmss.startvm(vmstring) 252 | statusmsg(current_vmss.status) 253 | refresh_thread_running = True 254 | 255 | 256 | def restartvm(): 257 | global refresh_thread_running 258 | vmid = vmtext.get() 259 | vmstring = '["' + vmid + '"]' 260 | current_vmss.restartvm(vmstring) 261 | statusmsg(current_vmss.status) 262 | refresh_thread_running = True 263 | 264 | 265 | def deallocvm(): 266 | global refresh_thread_running 267 | vmid = vmtext.get() 268 | vmstring = '["' + vmid + '"]' 269 | current_vmss.deallocvm(vmstring) 270 | statusmsg(current_vmss.status) 271 | refresh_thread_running = True 272 | 273 | 274 | def poweroffvm(): 275 | global refresh_thread_running 276 | vmid = vmtext.get() 277 | vmstring = '["' + vmid + '"]' 278 | current_vmss.poweroffvm(vmstring) 279 | statusmsg(current_vmss.status) 280 | refresh_thread_running = True 281 | 282 | 283 | # begin tkinter components 284 | root = tk.Tk() # Makes the window 285 | root.wm_title("Azure VM Scale Set Editor") 286 | root.geometry(geometry1) 287 | root.configure(background = frame_bgcolor) 288 | root.wm_iconbitmap('vmss.ico') 289 | topframe = tk.Frame(root, bg = frame_bgcolor) 290 | middleframe = tk.Frame(root, bg = frame_bgcolor) 291 | selectedfd = tk.StringVar() 292 | vmcanvas = tk.Canvas(middleframe, height=195, width=530, bg = canvas_bgcolor) 293 | vmframe = tk.Frame(root, bg = frame_bgcolor) 294 | baseframe = tk.Frame(root, bg = frame_bgcolor) 295 | topframe.pack(fill=tk.X) 296 | middleframe.pack(fill=tk.X) 297 | 298 | # Rolling upgrade operations - VM frame 299 | batchsizelabel= tk.Label(vmframe, text='Batch size:', bg = frame_bgcolor) 300 | batchtext = tk.Entry(vmframe, width=11, bg = canvas_bgcolor) 301 | batchtext.delete(0, tk.END) 302 | batchtext.insert(0, '1') 303 | pausetimelabel= tk.Label(vmframe, text='Pause time:', bg = frame_bgcolor) 304 | pausetext = tk.Entry(vmframe, width=11, bg = canvas_bgcolor) 305 | pausetext.delete(0, tk.END) 306 | pausetext.insert(0, '0') 307 | rollingbtn = tk.Button(vmframe, text='Rolling upgrade', command=rollingupgrade, width=btnwidth, bg = btncolor) 308 | 309 | # FD operations - VM frame 310 | fdlabel = tk.Label(vmframe, text='FD:', bg = frame_bgcolor) 311 | fdoption = tk.OptionMenu(vmframe, selectedfd, '0', '1', '2', '3', '4') 312 | fdoption.config(width=6, bg = btncolor, activebackground = btncolor) 313 | fdoption["menu"].config(bg=btncolor) 314 | reimagebtnfd = tk.Button(vmframe, text='Reimage', command=reimagefd, width=btnwidth, bg = btncolor) 315 | upgradebtnfd = tk.Button(vmframe, text='Upgrade', command=upgradefd, width=btnwidth, bg = btncolor) 316 | startbtnfd = tk.Button(vmframe, text='Start', command=startfd, width=btnwidth, bg = btncolor) 317 | powerbtnfd = tk.Button(vmframe, text='Power off', command=powerfd, width=btnwidth, bg = btncolor) 318 | 319 | # VM operations - VM frame 320 | vmlabel = tk.Label(vmframe, text='VM:', bg = frame_bgcolor) 321 | vmtext = tk.Entry(vmframe, width=11, bg = canvas_bgcolor) 322 | reimagebtn = tk.Button(vmframe, text='Reimage', command=reimagevm, width=btnwidth, bg = btncolor) 323 | vmupgradebtn = tk.Button(vmframe, text='Upgrade', command=upgradevm, width=btnwidth, bg = btncolor) 324 | vmdeletebtn = tk.Button(vmframe, text='Delete', command=deletevm, width=btnwidth, bg = btncolor) 325 | vmstartbtn = tk.Button(vmframe, text='Start', command=startvm, width=btnwidth, bg = btncolor) 326 | vmrestartbtn = tk.Button(vmframe, text='Restart', command=restartvm, width=btnwidth, bg = btncolor) 327 | vmdeallocbtn = tk.Button(vmframe, text='Dealloc', command=deallocvm, width=btnwidth, bg = btncolor) 328 | vmpoweroffbtn = tk.Button(vmframe, text='Power off', command=poweroffvm, width=btnwidth, bg = btncolor) 329 | vmframe.pack(fill=tk.X) 330 | baseframe.pack(fill=tk.X) 331 | 332 | versiontext = tk.Entry(topframe, width=entrywidth, bg = canvas_bgcolor) 333 | capacitytext = tk.Entry(topframe, width=entrywidth, bg = canvas_bgcolor) 334 | vmsizetext = tk.Entry(topframe, width=entrywidth, bg = canvas_bgcolor) 335 | statustext = tk.Text(baseframe, height=1, width=67, bg = canvas_bgcolor) 336 | 337 | 338 | def statusmsg(statusstring): 339 | if statustext.get(1.0, tk.END): 340 | statustext.delete(1.0, tk.END) 341 | statustext.insert(tk.END, statusstring) 342 | 343 | 344 | def displayvmss(vmssname): 345 | global current_vmss 346 | current_vmss = vmss.vmss(vmssname, sub.vmssdict[vmssname], sub.sub_id, sub.access_token) 347 | # capacity - row 0 348 | locationlabel = tk.Label(topframe, text=current_vmss.location, width=btnwidth, justify=tk.LEFT, bg = frame_bgcolor) 349 | locationlabel.grid(row=0, column=1, sticky=tk.W) 350 | tk.Label(topframe, text='Capacity: ', bg = frame_bgcolor).grid(row=0, column=2) 351 | capacitytext.grid(row=0, column=3, sticky=tk.W) 352 | capacitytext.delete(0, tk.END) 353 | capacitytext.insert(0, str(current_vmss.capacity)) 354 | scalebtn = tk.Button(topframe, text="Scale", command=scalevmss, width=btnwidth, bg = btncolor) 355 | scalebtn.grid(row=0, column=4, sticky=tk.W) 356 | 357 | # VMSS properties - row 1 358 | vmsizetext.grid(row=1, column=3, sticky=tk.W) 359 | vmsizetext.delete(0, tk.END) 360 | vmsizetext.insert(0, str(current_vmss.vmsize)) 361 | vmsizetext.grid(row=1, column=0, sticky=tk.W) 362 | offerlabel = tk.Label(topframe, text=current_vmss.offer, width=btnwidth, justify=tk.LEFT, bg = frame_bgcolor) 363 | offerlabel.grid(row=1, column=1, sticky=tk.W) 364 | skulabel = tk.Label(topframe, text=current_vmss.sku, width=btnwidth, justify=tk.LEFT, bg = frame_bgcolor) 365 | skulabel.grid(row=1, column=2, sticky=tk.W) 366 | versiontext.grid(row=1, column=3, sticky=tk.W) 367 | versiontext.delete(0, tk.END) 368 | versiontext.insert(0, current_vmss.version) 369 | updatebtn = tk.Button(topframe, text='Update model', command=updatevmss, width=btnwidth, bg = btncolor) 370 | updatebtn.grid(row=1, column=4, sticky=tk.W) 371 | 372 | # more VMSS properties - row 2 373 | if current_vmss.overprovision == True: 374 | optext = "overprovision: true" 375 | else: 376 | optext = "overprovision: false" 377 | overprovisionlabel = tk.Label(topframe, text=optext, width=btnwidth, justify=tk.LEFT, bg=frame_bgcolor) 378 | overprovisionlabel.grid(row=2, column=0, sticky=tk.W) 379 | upgradepolicylabel = tk.Label(topframe, text=current_vmss.upgradepolicy + ' upgrade', width=btnwidth, justify=tk.LEFT, bg=frame_bgcolor) 380 | upgradepolicylabel.grid(row=2, column=1, sticky=tk.W) 381 | adminuserlabel = tk.Label(topframe, text=current_vmss.adminuser, width=btnwidth, justify=tk.LEFT, bg=frame_bgcolor) 382 | adminuserlabel.grid(row=2, column=2, sticky=tk.W) 383 | compnameprefixlabel = tk.Label(topframe, text='Prefix: ' + current_vmss.nameprefix, width=btnwidth, justify=tk.LEFT, bg=frame_bgcolor) 384 | compnameprefixlabel.grid(row=2, column=3, sticky=tk.W) 385 | 386 | # vmss operations - row 3 387 | onbtn = tk.Button(topframe, text="Start", command=poweronvmss, width=btnwidth, bg = btncolor) 388 | onbtn.grid(row=3, column=0, sticky=tk.W) 389 | onbtn = tk.Button(topframe, text="Restart", command=restartvmss, width=btnwidth, bg = btncolor) 390 | onbtn.grid(row=3, column=1, sticky=tk.W) 391 | offbtn = tk.Button(topframe, text="Power off", command=poweroffvmss, width=btnwidth, bg = btncolor) 392 | offbtn.grid(row=3, column=2, sticky=tk.W) 393 | deallocbtn = tk.Button(topframe, text="Stop Dealloc", command=deallocvmss, width=btnwidth, bg = btncolor) 394 | deallocbtn.grid(row=3, column=3, sticky=tk.W) 395 | detailsbtn = tk.Button(topframe, text="Show Details", command=vmssdetails, width=btnwidth, bg = btncolor) 396 | detailsbtn.grid(row=3, column=4, sticky=tk.W) 397 | 398 | # status line 399 | statustext.pack() 400 | statusmsg(current_vmss.status) 401 | 402 | 403 | def scalevmss(): 404 | global refresh_thread_running 405 | newcapacity = int(capacitytext.get()) 406 | current_vmss.scale(newcapacity) 407 | statusmsg(current_vmss.status) 408 | refresh_thread_running = True 409 | 410 | 411 | def updatevmss(): 412 | global refresh_thread_running 413 | newversion = versiontext.get() 414 | newvmsize = vmsizetext.get() 415 | current_vmss.update_model(newversion, newvmsize) 416 | statusmsg(current_vmss.status) 417 | refresh_thread_running = True 418 | 419 | 420 | def poweronvmss(): 421 | global refresh_thread_running 422 | current_vmss.poweron() 423 | statusmsg(current_vmss.status) 424 | refresh_thread_running = True 425 | 426 | def restartvmss(): 427 | global refresh_thread_running 428 | current_vmss.restart() 429 | statusmsg(current_vmss.status) 430 | refresh_thread_running = True 431 | 432 | def poweroffvmss(): 433 | global refresh_thread_running 434 | current_vmss.poweroff() 435 | statusmsg(current_vmss.status) 436 | refresh_thread_running = True 437 | 438 | 439 | def deallocvmss(): 440 | global refresh_thread_running 441 | current_vmss.dealloc() 442 | statusmsg(current_vmss.status) 443 | refresh_thread_running = True 444 | 445 | 446 | def vmssdetails(): 447 | # VMSS VM canvas - middle frame 448 | root.geometry(geometry2) 449 | vmcanvas.pack() 450 | current_vmss.init_vm_instance_view() 451 | draw_vms(current_vmss.vm_instance_view) 452 | 453 | # draw rollingframe components 454 | batchsizelabel.grid(row=0, column=1, sticky=tk.W) 455 | batchtext.grid(row=0, column=2, sticky=tk.W) 456 | pausetimelabel.grid(row=0, column=3, sticky=tk.W) 457 | pausetext.grid(row=0, column=4, sticky=tk.W) 458 | rollingbtn.grid(row=0, column=5, sticky=tk.W) 459 | 460 | # draw VM frame components 461 | fdlabel.grid(row=1, column=0, sticky=tk.W) 462 | fdoption.grid(row=1, column=1, sticky=tk.W) 463 | reimagebtnfd.grid(row=1, column=2, sticky=tk.W) 464 | upgradebtnfd.grid(row=1, column=3, sticky=tk.W) 465 | startbtnfd.grid(row=1, column=4, sticky=tk.W) 466 | powerbtnfd.grid(row=1, column=5, sticky=tk.W) 467 | vmlabel.grid(row=2, column=0, sticky=tk.W) 468 | vmtext.grid(row=2, column=1, sticky=tk.W) 469 | reimagebtn.grid(row=2, column=2, sticky=tk.W) 470 | vmupgradebtn.grid(row=2, column=3, sticky=tk.W) 471 | vmstartbtn.grid(row=2, column=4, sticky=tk.W) 472 | vmpoweroffbtn.grid(row=2, column=5, sticky=tk.W) 473 | vmdeletebtn.grid(row=3, column=2, sticky=tk.W) 474 | vmrestartbtn.grid(row=3, column=3, sticky=tk.W) 475 | vmdeallocbtn.grid(row=3, column=4, sticky=tk.W) 476 | 477 | # draw status frame 478 | statusmsg(current_vmss.status) 479 | 480 | # start by listing VM Scale Sets 481 | vmsslist = sub.get_vmss_list() 482 | selectedvmss = tk.StringVar() 483 | if len(vmsslist) > 0: 484 | selectedvmss.set(vmsslist[0]) 485 | selectedfd.set('0') 486 | displayvmss(vmsslist[0]) 487 | # create top level GUI components 488 | vmsslistoption = tk.OptionMenu(topframe, selectedvmss, *vmsslist, command=displayvmss) 489 | vmsslistoption.config(width=8, bg = btncolor, activebackground = btncolor) 490 | vmsslistoption["menu"].config(bg=btncolor) 491 | vmsslistoption.grid(row=0, column=0, sticky=tk.W) 492 | else: 493 | messagebox.showwarning("Warning", "Your subscription:\n" + sub.sub_id + "\ncontains no VM Scale Sets") 494 | 495 | root.mainloop() 496 | --------------------------------------------------------------------------------