├── images ├── runaslist.png ├── runasname.png └── runasconnection.png ├── LICENSE ├── automationassets ├── localassets.json └── automationassets.py └── README.md /images/runaslist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azureautomation/python_emulated_assets/HEAD/images/runaslist.png -------------------------------------------------------------------------------- /images/runasname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azureautomation/python_emulated_assets/HEAD/images/runasname.png -------------------------------------------------------------------------------- /images/runasconnection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azureautomation/python_emulated_assets/HEAD/images/runasconnection.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Azure Automation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /automationassets/localassets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Variable": [ 3 | { 4 | "Name": "myvariable", 5 | "Value": "myvariablevalue" 6 | }, 7 | { 8 | "Name": "myvariable1", 9 | "Value": "myvariablevalue1" 10 | } 11 | ], 12 | "Credential": [ 13 | { 14 | "Username": "user", 15 | "Password": "password", 16 | "Name": "mycredential" 17 | }, 18 | { 19 | "Username": "user1", 20 | "Password": "password1", 21 | "Name": "mycredential1" 22 | } 23 | ], 24 | "Connection": [ 25 | { 26 | "ValueFields": { 27 | "SubscriptionId": "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", 28 | "ApplicationId": "XXXX-XXXX-XXXX-XXXX-XXXXXXXX", 29 | "CertificateThumbprint": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 30 | "TenantId": "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 31 | }, 32 | "Name": "AzureRunAsConnection", 33 | "ConnectionType": "AzureServicePrincipal" 34 | } 35 | ], 36 | "Certificate": [ 37 | { 38 | "Thumbprint": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 39 | "CertPath": "/home/user/automation/myrunas.pfx", 40 | "Password": "StrongPassword", 41 | "Exportable": true, 42 | "Name": "AzureRunAsCertificate" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /automationassets/automationassets.py: -------------------------------------------------------------------------------- 1 | """ Azure Automation assets module to be used with Azure Automation during offline development """ 2 | #!/usr/bin/env python2 3 | # ---------------------------------------------------------------------------------- 4 | # 5 | # MIT License 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # ---------------------------------------------------------------------------------- 25 | 26 | # Constant keys for extracing items from automation assets. 27 | _KEY_NAME = "Name" 28 | _KEY_VALUE = "Value" 29 | _KEY_USERNAME = "Username" 30 | _KEY_PASSWORD = "Password" 31 | _KEY_CERTPATH = "CertPath" 32 | _KEY_CONNECTION_FIELDS = "ValueFields" 33 | 34 | # Assets supported in Azure automation within python scripts 35 | _KEY_VARIABLE = "Variable" 36 | _KEY_CERTIFICATE = "Certificate" 37 | _KEY_CREDENTIAL = "Credential" 38 | _KEY_CONNECTION = "Connection" 39 | 40 | # Get Azure Automation asset json file 41 | def _get_automation_asset_file(): 42 | import os 43 | if os.environ.get('AUTOMATION_ASSET_FILE') is not None: 44 | return os.environ.get('AUTOMATION_ASSET_FILE') 45 | return os.path.join(os.path.dirname(__file__), "localassets.json") 46 | 47 | # Helper function to find an asset of a specific type and name in the asset file 48 | def _get_asset_value(asset_file, asset_type, asset_name): 49 | import json 50 | json_data = open(asset_file) 51 | json_string = json_data.read() 52 | local_assets = json.loads(json_string) 53 | 54 | return_value = None 55 | for asset, asset_values in local_assets.iteritems(): 56 | if asset == asset_type: 57 | for value in asset_values: 58 | if value[_KEY_NAME] == asset_name: 59 | return_value = value 60 | break 61 | if return_value != None: 62 | # Found the value so break out of loop 63 | break 64 | 65 | return return_value 66 | 67 | # Returns an asset from the asses file 68 | def _get_asset(asset_type, asset_name): 69 | local_assets_file = _get_automation_asset_file() 70 | 71 | # Look in assets file for value 72 | return_value = _get_asset_value(local_assets_file, asset_type, asset_name) 73 | 74 | if return_value is None: 75 | raise LookupError("asset:" + asset_name + " not found") 76 | return return_value 77 | 78 | # Helper function to set an asset of a specific type and name in the assetFile 79 | def _set_asset_value(asset_file, asset_type, asset_name, asset_value): 80 | import json 81 | json_data = open(asset_file) 82 | json_string = json_data.read() 83 | local_assets = json.loads(json_string) 84 | item_found = False 85 | 86 | for asset, asset_values in local_assets.iteritems(): 87 | if asset == asset_type: 88 | for value in asset_values: 89 | if value[_KEY_NAME] == asset_name: 90 | value[_KEY_VALUE] = asset_value 91 | with open(asset_file, 'w') as asset_file_content: 92 | asset_file_content.write(json.dumps(local_assets, indent=4)) 93 | item_found = True 94 | break 95 | 96 | if item_found: 97 | break 98 | 99 | return item_found 100 | 101 | # Updates an asset in the assets file 102 | def _set_asset(asset_type, asset_name, asset_value): 103 | local_assets_file = _get_automation_asset_file() 104 | # Check assets file for value. 105 | item_found = _set_asset_value(local_assets_file, 106 | asset_type, asset_name, asset_value) 107 | 108 | if item_found is False: 109 | raise LookupError("asset:" + asset_name + " not found") 110 | 111 | # Below are the 5 supported calls that can be made to automation assets from within 112 | # a python script 113 | def get_automation_variable(name): 114 | """ Returns an automation variable """ 115 | variable = _get_asset(_KEY_VARIABLE, name) 116 | return variable[_KEY_VALUE] 117 | 118 | 119 | def set_automation_variable(name, value): 120 | """ Sets an automation variable """ 121 | _set_asset(_KEY_VARIABLE, name, value) 122 | 123 | 124 | def get_automation_credential(name): 125 | """ Returns an automation credential as a dictionay with username and password as keys """ 126 | credential = _get_asset(_KEY_CREDENTIAL, name) 127 | 128 | # Return a dictionary of the credential asset 129 | credential_dictionary = {} 130 | credential_dictionary['username'] = credential['Username'] 131 | credential_dictionary['password'] = credential['Password'] 132 | return credential_dictionary 133 | 134 | 135 | def get_automation_connection(name): 136 | """ Returns an automation connection dictionary """ 137 | connection = _get_asset(_KEY_CONNECTION, name) 138 | return connection[_KEY_CONNECTION_FIELDS] 139 | 140 | 141 | def get_automation_certificate(name): 142 | """ Returns an automation certificate in PKCS12 bytes """ 143 | from OpenSSL import crypto 144 | certificate = _get_asset(_KEY_CERTIFICATE, name) 145 | pks12_cert = crypto.load_pkcs12(open(certificate[_KEY_CERTPATH], 'rb').read(), 146 | certificate[_KEY_PASSWORD]) 147 | return crypto.PKCS12.export(pks12_cert) 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure Automation python emulated assets 3 | 4 | This python module enables the development and testing of Azure Automation python runbooks in an offline experience using the built-in Automation assets (variables, credentials, connections, and certificates). You can learn more about [python support](https://docs.microsoft.com/en-us/azure/automation/automation-first-runbook-textual-python2) in the Azure Automation documentation. 5 | 6 | This emulated module supports the following functions: 7 | * get_automation_variable 8 | * set_automation_variable 9 | * get_automation_credential 10 | * get_automation_connection 11 | * get_automation_certificate 12 | 13 | The local values should be entered into the [localassets.json](automationassets/localassets.json) file as these values are read by the emulated python automationassets module functions. You should keep this file secure if it contains sensitive information. The functions will look in the localassets.json for values in the same directory as the automationassets.py by default. You can set an environment variable called AUTOMATION_ASSET_FILE with a different location for this file if you prefer. 14 | 15 | ### Using the emulated automationassets module 16 | 17 | * Copy the [automationassets](automationassets/automationassets.py) and [localassets](automationassets/localassets.json) files into the directory where you are authoring python runbooks. 18 | * Author python runbooks and call the previous functions like you would when running within the Automation service. 19 | 20 | * For example, you should be able to create a new python file with the following code and run it locally. 21 | 22 | ```python 23 | import automationassets 24 | 25 | print automationassets.get_automation_variable("myvariable") 26 | 27 | cred = automationassets.get_automation_credential("mycredential") 28 | print cred['username'] 29 | print cred['password'] 30 | ``` 31 | 32 | You can get additional [sample python runbooks](https://github.com/azureautomation/runbooks/tree/master/Utility/Python) from github that shows how to use the automation assets. 33 | 34 | ## Enabling use of Azure Automation RunAs account locally for authenticating to Azure 35 | 36 | In order to authenticate with Azure resources, Azure Automation creates a [RunAs service principal](https://docs.microsoft.com/en-us/azure/automation/automation-create-runas-account). This service principal uses certificate-based authentication. Use the following steps to add another certificate to the service principal that is used for authentication from the development machine. 37 | 38 | ### Install Azure CLI 39 | 40 | * Download and install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) on your Windows or Linux computer. 41 | * Authenticate to your Azure subscription by running: 42 | 43 | ```bash 44 | az login 45 | ``` 46 | 47 | ### Create new certificate and add it to the existing service principal used for the automation account 48 | 49 | * Get the Azure Automation RunAs service principal name from the Azure Automation account 50 | 51 | ![RunAs account in Automation](images/runaslist.png) 52 | ![RunAs account name in Automation](images/runasname.png) 53 | 54 | * Create and add a new certificate to this service principal using the name copied from the portal. You need to be the owner of this service principal or be an administrator in the Azure active directory. If you created the RunAs during Automation account creation, then you are an owner. 55 | 56 | ```bash 57 | az ad sp credential reset --name ignite2017_jVI6s7/RI0PAAXv4A33BCDDYY12= --append --create-cert 58 | ``` 59 | 60 | ### Get the thumbprint of certificate created. The location of the certificate file is returned by the previous call if successful 61 | 62 | ```bash 63 | openssl x509 -noout -in /home/user/tmpbtxnq3vs.pem -fingerprint 64 | ``` 65 | 66 | ### Export the certificate as pkcs12 since that is used within Automation accounts for the RunAs. Specify any password when prompted. 67 | 68 | ```bash 69 | openssl pkcs12 -export -in /home/user/runas.pem -out hybrid_runas.pfx 70 | ``` 71 | 72 | * You can now copy the thumbprint, location of the pfx file, and the password used into the certificate object in [localassets.json](automationassets/localassets.json) 73 | 74 | ### Update the AzureRunAsConnection object to the correct values in localassets.json 75 | 76 | * Get the ApplicationId, SubscriptionId, and TenantId from the RunAs account in the Azure Automation account in the portal. 77 | 78 | ![RunAs connection properties](images/runasconnection.png) 79 | 80 | * Modify the AzureRunAsConnection object in the [localassets.json](automationassets/localassets.json) file with these values. Update the thumbprint with the value of your local thumbprint and not the one from the portal. 81 | 82 | ### Testing the local RunAs service principal 83 | 84 | * Install the latest Azure python sdk 85 | 86 | ```python 87 | pip install --pre azure 88 | ``` 89 | * Create a new python script from the following code and run it 90 | 91 | ```python 92 | """ Tutorial to show how to authenticate against Azure resource manager resources """ 93 | import azure.mgmt.resource 94 | import automationassets 95 | 96 | 97 | def get_automation_runas_credential(runas_connection): 98 | """ Returns credentials to authenticate against Azure resoruce manager """ 99 | from OpenSSL import crypto 100 | from msrestazure import azure_active_directory 101 | import adal 102 | 103 | # Get the Azure Automation RunAs service principal certificate 104 | cert = automationassets.get_automation_certificate("AzureRunAsCertificate") 105 | pks12_cert = crypto.load_pkcs12(cert) 106 | pem_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pks12_cert.get_privatekey()) 107 | 108 | # Get run as connection information for the Azure Automation service principal 109 | application_id = runas_connection["ApplicationId"] 110 | thumbprint = runas_connection["CertificateThumbprint"] 111 | tenant_id = runas_connection["TenantId"] 112 | 113 | # Authenticate with service principal certificate 114 | resource = "https://management.core.windows.net/" 115 | authority_url = ("https://login.microsoftonline.com/" + tenant_id) 116 | context = adal.AuthenticationContext(authority_url) 117 | return azure_active_directory.AdalAuthentication( 118 | lambda: context.acquire_token_with_client_certificate( 119 | resource, 120 | application_id, 121 | pem_pkey, 122 | thumbprint) 123 | ) 124 | 125 | 126 | # Authenticate to Azure using the Azure Automation RunAs service principal 127 | runas_connection = automationassets.get_automation_connection("AzureRunAsConnection") 128 | azure_credential = get_automation_runas_credential(runas_connection) 129 | 130 | # Intialize the resource management client with the RunAs credential and subscription 131 | resource_client = azure.mgmt.resource.ResourceManagementClient( 132 | azure_credential, 133 | str(runas_connection["SubscriptionId"])) 134 | 135 | # Get list of resource groups and print them out 136 | groups = resource_client.resource_groups.list() 137 | for group in groups: 138 | print group.name 139 | ``` 140 | --------------------------------------------------------------------------------