diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py index f5d04317c2b..9b1b966e88e 100644 --- a/lib/ansible/module_utils/azure_rm_common.py +++ b/lib/ansible/module_utils/azure_rm_common.py @@ -205,7 +205,7 @@ def normalize_location_name(name): AZURE_PKG_VERSIONS = { 'StorageManagementClient': { 'package_name': 'storage', - 'expected_version': '1.5.0' + 'expected_version': '3.1.0' }, 'ComputeManagementClient': { 'package_name': 'compute', @@ -379,6 +379,7 @@ class AzureRMModuleBase(object): :param tags: metadata tags from the object :return: bool, dict ''' + tags = tags or dict() new_tags = copy.copy(tags) if isinstance(tags, dict) else dict() param_tags = self.module.params.get('tags') if isinstance(self.module.params.get('tags'), dict) else dict() append_tags = self.module.params.get('append_tags') if self.module.params.get('append_tags') is not None else True @@ -790,12 +791,12 @@ class AzureRMModuleBase(object): if not self._storage_client: self._storage_client = self.get_mgmt_svc_client(StorageManagementClient, base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2017-10-01') + api_version='2018-07-01') return self._storage_client @property def storage_models(self): - return StorageManagementClient.models("2017-10-01") + return StorageManagementClient.models("2018-07-01") @property def network_client(self): diff --git a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py index 8a17b6c2a95..9ab8d45fc11 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py @@ -54,6 +54,7 @@ options: - StandardSSD_LRS - Standard_RAGRS - Standard_ZRS + - Premium_ZRS aliases: - type custom_domain: @@ -62,6 +63,8 @@ options: keys where 'name' is the CNAME source. Only one custom domain is supported per storage account at this time. To clear the existing custom domain, use an empty string for the custom domain name property. - Can be added to an existing storage account. Will be ignored during storage account creation. + aliases: + - custom_dns_domain_suffix kind: description: - The 'kind' of storage. @@ -78,10 +81,51 @@ options: - Hot - Cool version_added: "2.4" - force: + force_delete_nonempty: description: - Attempt deletion if resource already exists and cannot be updated type: bool + aliases: + - force + https_only: + description: + - Allows https traffic only to storage service if sets to true. + type: bool + version_added: "2.8" + blob_cors: + description: + - Specifies CORS rules for the Blob service. + - You can include up to five CorsRule elements in the request. + - If no blob_cors elements are included in the argument list, nothing about CORS will be changed. + - "If you want to delete all CORS rules and disable CORS for the Blob service, explicitly set blob_cors: []." + type: list + version_added: "2.8" + suboptions: + allowed_origins: + description: + - A list of origin domains that will be allowed via CORS, or "*" to allow all domains. + type: list + required: true + allowed_methods: + description: + - A list of HTTP methods that are allowed to be executed by the origin. + type: list + required: true + max_age_in_seconds: + description: + - The number of seconds that the client/browser should cache a preflight response. + type: int + required: true + exposed_headers: + description: + - A list of response headers to expose to CORS clients. + type: list + required: true + allowed_headers: + description: + - A list of headers allowed to be part of the cross-origin request. + type: list + required: true extends_documentation_fragment: - azure @@ -90,7 +134,6 @@ extends_documentation_fragment: author: - "Chris Houseknecht (@chouseknecht)" - "Matt Davis (@nitzmahone)" - ''' EXAMPLES = ''' @@ -108,6 +151,25 @@ EXAMPLES = ''' tags: testing: testing delete: on-exit + + - name: create an account with blob CORS + azure_rm_storageaccount: + resource_group: Testing + name: clh002 + type: Standard_RAGRS + blob_cors: + - allowed_origins: + - http://www.example.com/ + allowed_methods: + - GET + - POST + allowed_headers: + - x-ms-meta-data* + - x-ms-meta-target* + - x-ms-meta-abc + exposed_headers: + - x-ms-meta-* + max_age_in_seconds: 200 ''' @@ -151,7 +213,36 @@ except ImportError: # This is handled in azure_rm_common pass -from ansible.module_utils.azure_rm_common import AZURE_SUCCESS_STATE, AzureRMModuleBase, HAS_AZURE +import copy +from ansible.module_utils.azure_rm_common import AZURE_SUCCESS_STATE, AzureRMModuleBase +from ansible.module_utils._text import to_native + +cors_rule_spec = dict( + allowed_origins=dict(type='list', elements='str', required=True), + allowed_methods=dict(type='list', elements='str', required=True), + max_age_in_seconds=dict(type='int', required=True), + exposed_headers=dict(type='list', elements='str', required=True), + allowed_headers=dict(type='list', elements='str', required=True), +) + + +def compare_cors(cors1, cors2): + if len(cors1) != len(cors2): + return False + copy2 = copy.copy(cors2) + for rule1 in cors1: + matched = False + for rule2 in copy2: + if (rule1['max_age_in_seconds'] == rule2['max_age_in_seconds'] + and set(rule1['allowed_methods']) == set(rule2['allowed_methods']) + and set(rule1['allowed_origins']) == set(rule2['allowed_origins']) + and set(rule1['allowed_headers']) == set(rule2['allowed_headers']) + and set(rule1['exposed_headers']) == set(rule2['exposed_headers'])): + matched = True + copy2.remove(rule2) + if not matched: + return False + return True class AzureRMStorageAccount(AzureRMModuleBase): @@ -159,24 +250,22 @@ class AzureRMStorageAccount(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( - account_type=dict(type='str', choices=['Premium_LRS', 'Standard_GRS', 'Standard_LRS', 'StandardSSD_LRS', 'Standard_RAGRS', 'Standard_ZRS'], + account_type=dict(type='str', + choices=['Premium_LRS', 'Standard_GRS', 'Standard_LRS', 'StandardSSD_LRS', 'Standard_RAGRS', 'Standard_ZRS', 'Premium_ZRS'], aliases=['type']), - custom_domain=dict(type='dict'), + custom_domain=dict(type='dict', aliases=['custom_dns_domain_suffix']), location=dict(type='str'), name=dict(type='str', required=True), resource_group=dict(required=True, type='str', aliases=['resource_group_name']), state=dict(default='present', choices=['present', 'absent']), - force=dict(type='bool', default=False), + force_delete_nonempty=dict(type='bool', default=False, aliases=['force']), tags=dict(type='dict'), kind=dict(type='str', default='Storage', choices=['Storage', 'StorageV2', 'BlobStorage']), - access_tier=dict(type='str', choices=['Hot', 'Cool']) + access_tier=dict(type='str', choices=['Hot', 'Cool']), + https_only=dict(type='bool', default=False), + blob_cors=dict(type='list', options=cors_rule_spec, elements='dict') ) - if HAS_AZURE: - for key in self.storage_models.SkuName: - if getattr(key, 'value') not in self.module_arg_spec['account_type']['choices']: - self.module_arg_spec['account_type']['choices'].append(getattr(key, 'value')) - self.results = dict( changed=False, state=dict() @@ -190,9 +279,11 @@ class AzureRMStorageAccount(AzureRMModuleBase): self.account_type = None self.custom_domain = None self.tags = None - self.force = None + self.force_delete_nonempty = None self.kind = None self.access_tier = None + self.https_only = None + self.blob_cors = None super(AzureRMStorageAccount, self).__init__(self.module_arg_spec, supports_check_mode=True) @@ -254,19 +345,21 @@ class AzureRMStorageAccount(AzureRMModuleBase): def get_account(self): self.log('Get properties for account {0}'.format(self.name)) account_obj = None + blob_service_props = None account_dict = None try: account_obj = self.storage_client.storage_accounts.get_properties(self.resource_group, self.name) + blob_service_props = self.storage_client.blob_services.get_service_properties(self.resource_group, self.name) except CloudError: pass if account_obj: - account_dict = self.account_obj_to_dict(account_obj) + account_dict = self.account_obj_to_dict(account_obj, blob_service_props) return account_dict - def account_obj_to_dict(self, account_obj): + def account_obj_to_dict(self, account_obj, blob_service_props=None): account_dict = dict( id=account_obj.id, name=account_obj.name, @@ -283,7 +376,8 @@ class AzureRMStorageAccount(AzureRMModuleBase): if account_obj.status_of_primary is not None else None), status_of_secondary=(account_obj.status_of_secondary.value if account_obj.status_of_secondary is not None else None), - primary_location=account_obj.primary_location + primary_location=account_obj.primary_location, + https_only=account_obj.enable_https_traffic_only ) account_dict['custom_domain'] = None if account_obj.custom_domain: @@ -309,10 +403,30 @@ class AzureRMStorageAccount(AzureRMModuleBase): account_dict['tags'] = None if account_obj.tags: account_dict['tags'] = account_obj.tags + if blob_service_props and blob_service_props.cors and blob_service_props.cors.cors_rules: + account_dict['blob_cors'] = [dict( + allowed_origins=[to_native(y) for y in x.allowed_origins], + allowed_methods=[to_native(y) for y in x.allowed_methods], + max_age_in_seconds=x.max_age_in_seconds, + exposed_headers=[to_native(y) for y in x.exposed_headers], + allowed_headers=[to_native(y) for y in x.allowed_headers] + ) for x in blob_service_props.cors.cors_rules] return account_dict def update_account(self): self.log('Update storage account {0}'.format(self.name)) + if bool(self.https_only) != bool(self.account_dict.get('https_only')): + self.results['changed'] = True + self.account_dict['https_only'] = self.https_only + if not self.check_mode: + try: + parameters = self.storage_models.StorageAccountUpdateParameters(enable_https_traffic_only=self.https_only) + self.storage_client.storage_accounts.update(self.resource_group, + self.name, + parameters) + except Exception as exc: + self.fail("Failed to update account type: {0}".format(str(exc))) + if self.account_type: if self.account_type != self.account_dict['sku_name']: # change the account type @@ -332,7 +446,7 @@ class AzureRMStorageAccount(AzureRMModuleBase): try: self.log("sku_name: %s" % self.account_dict['sku_name']) self.log("sku_tier: %s" % self.account_dict['sku_tier']) - sku = self.storage_models.Sku(SkuName(self.account_dict['sku_name'])) + sku = self.storage_models.Sku(name=SkuName(self.account_dict['sku_name'])) sku.tier = self.storage_models.SkuTier(self.account_dict['sku_tier']) parameters = self.storage_models.StorageAccountUpdateParameters(sku=sku) self.storage_client.storage_accounts.update(self.resource_group, @@ -377,6 +491,11 @@ class AzureRMStorageAccount(AzureRMModuleBase): except Exception as exc: self.fail("Failed to update tags: {0}".format(str(exc))) + if self.blob_cors and not compare_cors(self.account_dict.get('blob_cors', []), self.blob_cors): + self.results['changed'] = True + if not self.check_mode: + self.set_blob_cors() + def create_account(self): self.log("Creating account {0}".format(self.name)) @@ -398,16 +517,22 @@ class AzureRMStorageAccount(AzureRMModuleBase): account_type=self.account_type, name=self.name, resource_group=self.resource_group, + enable_https_traffic_only=self.https_only, tags=dict() ) if self.tags: account_dict['tags'] = self.tags + if self.blob_cors: + account_dict['blob_cors'] = self.blob_cors return account_dict - sku = self.storage_models.Sku(self.storage_models.SkuName(self.account_type)) + sku = self.storage_models.Sku(name=self.storage_models.SkuName(self.account_type)) sku.tier = self.storage_models.SkuTier.standard if 'Standard' in self.account_type else \ self.storage_models.SkuTier.premium - parameters = self.storage_models.StorageAccountCreateParameters(sku, self.kind, self.location, - tags=self.tags, access_tier=self.access_tier) + parameters = self.storage_models.StorageAccountCreateParameters(sku=sku, + kind=self.kind, + location=self.location, + tags=self.tags, + access_tier=self.access_tier) self.log(str(parameters)) try: poller = self.storage_client.storage_accounts.create(self.resource_group, self.name, parameters) @@ -415,13 +540,15 @@ class AzureRMStorageAccount(AzureRMModuleBase): except CloudError as e: self.log('Error creating storage account.') self.fail("Failed to create account: {0}".format(str(e))) + if self.blob_cors: + self.set_blob_cors() # the poller doesn't actually return anything return self.get_account() def delete_account(self): if self.account_dict['provisioning_state'] == self.storage_models.ProvisioningState.succeeded.value and \ - self.account_has_blob_containers() and self.force: - self.fail("Account contains blob containers. Is it in use? Use the force option to attempt deletion.") + not self.force_delete_nonempty and self.account_has_blob_containers(): + self.fail("Account contains blob containers. Is it in use? Use the force_delete_nonempty option to attempt deletion.") self.log('Delete storage account {0}'.format(self.name)) self.results['changed'] = True @@ -451,6 +578,15 @@ class AzureRMStorageAccount(AzureRMModuleBase): return True return False + def set_blob_cors(self): + try: + cors_rules = self.storage_models.CorsRules(cors_rules=[self.storage_models.CorsRule(**x) for x in self.blob_cors]) + self.storage_client.blob_services.set_service_properties(self.resource_group, + self.name, + self.storage_models.BlobServiceProperties(cors=cors_rules)) + except Exception as exc: + self.fail("Failed to set CORS rules: {0}".format(str(exc))) + def main(): AzureRMStorageAccount() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount_facts.py b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount_facts.py index 99838f85fd6..38e3500c911 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount_facts.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount_facts.py @@ -37,6 +37,18 @@ options: tags: description: - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. + show_connection_string: + description: + - Show the connection string for each of the storageaccount's endpoints. + - Note that it will cost a lot of time when list all storageaccount rather than query a single one. + type: bool + version_added: "2.8" + show_blob_cors: + description: + - Show the blob CORS settings for each of the storageaccount's blob. + - Note that it will cost a lot time when list all storageaccount rather than querry a single one. + type: bool + version_added: "2.8" extends_documentation_fragment: - azure @@ -89,6 +101,176 @@ azure_storageaccounts: "tags": {}, "type": "Microsoft.Storage/storageAccounts" }] +storageaccounts: + description: List of storage account dicts in resource module's parameter format. + returned: always + type: complex + contains: + id: + description: + - Resource ID. + sample: "/subscriptions/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/testing/providers/Microsoft.Storage/storageAccounts/testaccount001" + name: + description: + - Name of the storage account to update or create. + sample: "testaccount001" + location: + description: + - Valid azure location. Defaults to location of the resource group. + sample: eastus + account_type: + description: + - Type of storage account. + - "NOTE: Standard_ZRS and Premium_LRS accounts cannot be changed to other account types." + - Other account types cannot be changed to Standard_ZRS or Premium_LRS. + sample: Standard_ZRS + custom_domain: + description: + - User domain assigned to the storage account. + - Must be a dictionary with 'name' and 'use_sub_domain' keys where 'name' is the CNAME source. + type: complex + contains: + name: + description: + - CNAME source. + sample: testaccount + use_sub_domain: + description: + - whether to use sub domain. + sample: true + kind: + description: + - The 'kind' of storage. + sample: Storage + access_tier: + description: + - The access tier for this storage account. + sample: Hot + https_only: + description: + - Allows https traffic only to storage service if sets to true. + sample: false + provisioning_state: + description: + - Gets the status of the storage account at the time the operation was called. + - Possible values include 'Creating', 'ResolvingDNS', 'Succeeded'. + sample: Succeeded + secondary_location: + description: + - Gets the location of the geo-replicated secondary for the storage account. + - Only available if the accountType is Standard_GRS or Standard_RAGRS. + sample: westus + status_of_primary: + description: + - Gets the status indicating whether the primary location of the storage account is available or unavailable. + sample: available + status_of_secondary: + description: + - Gets the status indicating whether the secondary location of the storage account is available or unavailable. + sample: available + primary_location: + description: + - Gets the location of the primary data center for the storage account. + sample: eastus + primary_endpoints: + description: + - Gets the URLs that are used to perform a retrieval of a public blob, queue, or table object. + - Note that Standard_ZRS and Premium_LRS accounts only return the blob endpoint. + type: complex + contains: + blob: + description: + - Gets the primary blob endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the primary blob endpoint. + sample: "https://testaccount001.blob.core.windows.net/" + connectionstring: + description: + - Connectionstring of the blob endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;BlobEndpoint=X" + queue: + description: + - Gets the primary queue endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the primary queue endpoint. + sample: "https://testaccount001.queue.core.windows.net/" + connectionstring: + description: + - Connectionstring of the queue endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;QueueEndpoint=X" + table: + description: + - Gets the primary table endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the primary table endpoint. + sample: "https://testaccount001.table.core.windows.net/" + connectionstring: + description: + - Connectionstring of the table endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;TableEndpoint=X" + secondary_endpoints: + description: + - Gets the URLs that are used to perform a retrieval of a public blob, queue, or table object from the secondary location. + - Only available if the SKU name is Standard_RAGRS. + type: complex + contains: + blob: + description: + - Gets the secondary blob endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the secondary blob endpoint. + sample: "https://testaccount001.blob.core.windows.net/" + connectionstring: + description: + - Connectionstring of the blob endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;BlobEndpoint=X" + queue: + description: + - Gets the secondary queue endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the secondary queue endpoint. + sample: "https://testaccount001.queue.core.windows.net/" + connectionstring: + description: + - Connectionstring of the queue endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;QueueEndpoint=X" + table: + description: + - Gets the secondary table endpoint and connection string. + type: complex + contains: + endpoint: + description: + - Gets the secondary table endpoint. + sample: "https://testaccount001.table.core.windows.net/" + connectionstring: + description: + - Connectionstring of the table endpoint + sample: "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=X;AccountKey=X;TableEndpoint=X" + tags: + description: + - Resource tags. + type: dict + sample: { "tag1": "abc" } + blob_cors: + description: + - Blob CORS of blob. + type: list ''' try: @@ -98,6 +280,7 @@ except Exception: pass from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils._text import to_native AZURE_OBJECT_CLASS = 'StorageAccount' @@ -110,16 +293,21 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase): name=dict(type='str'), resource_group=dict(type='str', aliases=['resource_group_name']), tags=dict(type='list'), + show_connection_string=dict(type='bool'), + show_blob_cors=dict(type='bool') ) self.results = dict( changed=False, - ansible_facts=dict(azure_storageaccounts=[]) + ansible_facts=dict(azure_storageaccounts=[]), + storageaccounts=[] ) self.name = None self.resource_group = None self.tags = None + self.show_connection_string = None + self.show_blob_cors = None super(AzureRMStorageAccountFacts, self).__init__(self.module_arg_spec, supports_tags=False, @@ -133,29 +321,29 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase): if self.name and not self.resource_group: self.fail("Parameter error: resource group required when filtering by name.") + results = [] if self.name: - self.results['ansible_facts']['azure_storageaccounts'] = self.get_account() + results = self.get_account() elif self.resource_group: - self.results['ansible_facts']['azure_storageaccounts'] = self.list_resource_group() + results = self.list_resource_group() else: - self.results['ansible_facts']['azure_storageaccounts'] = self.list_all() + results = self.list_all() + filtered = self.filter_tag(results) + + self.results['ansible_facts']['azure_storageaccounts'] = self.serialize(filtered) + self.results['ansible_facts']['storageaccounts'] = self.format_to_dict(filtered) return self.results def get_account(self): self.log('Get properties for account {0}'.format(self.name)) account = None - result = [] - try: account = self.storage_client.storage_accounts.get_properties(self.resource_group, self.name) + return [account] except CloudError: pass - - if account and self.has_tags(account.tags, self.tags): - result = [self.serialize_obj(account, AZURE_OBJECT_CLASS)] - - return result + return [] def list_resource_group(self): self.log('List items') @@ -164,11 +352,7 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase): except Exception as exc: self.fail("Error listing for resource group {0} - {1}".format(self.resource_group, str(exc))) - results = [] - for item in response: - if self.has_tags(item.tags, self.tags): - results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS)) - return results + return response def list_all(self): self.log('List all items') @@ -177,11 +361,110 @@ class AzureRMStorageAccountFacts(AzureRMModuleBase): except Exception as exc: self.fail("Error listing all items - {0}".format(str(exc))) - results = [] - for item in response: - if self.has_tags(item.tags, self.tags): - results.append(self.serialize_obj(item, AZURE_OBJECT_CLASS)) - return results + return response + + def filter_tag(self, raw): + return [item for item in raw if self.has_tags(item.tags, self.tags)] + + def serialize(self, raw): + return [self.serialize_obj(item, AZURE_OBJECT_CLASS) for item in raw] + + def format_to_dict(self, raw): + return [self.account_obj_to_dict(item) for item in raw] + + def account_obj_to_dict(self, account_obj, blob_service_props=None): + account_dict = dict( + id=account_obj.id, + name=account_obj.name, + location=account_obj.location, + access_tier=(account_obj.access_tier.value + if account_obj.access_tier is not None else None), + account_type=account_obj.sku.name.value, + kind=account_obj.kind.value if account_obj.kind else None, + provisioning_state=account_obj.provisioning_state.value, + secondary_location=account_obj.secondary_location, + status_of_primary=(account_obj.status_of_primary.value + if account_obj.status_of_primary is not None else None), + status_of_secondary=(account_obj.status_of_secondary.value + if account_obj.status_of_secondary is not None else None), + primary_location=account_obj.primary_location, + https_only=account_obj.enable_https_traffic_only + ) + + id_dict = self.parse_resource_to_dict(account_obj.id) + account_dict['resource_group'] = id_dict.get('resource_group') + account_key = self.get_connectionstring(account_dict['resource_group'], account_dict['name']) + account_dict['custom_domain'] = None + if account_obj.custom_domain: + account_dict['custom_domain'] = dict( + name=account_obj.custom_domain.name, + use_sub_domain=account_obj.custom_domain.use_sub_domain + ) + + account_dict['primary_endpoints'] = None + if account_obj.primary_endpoints: + account_dict['primary_endpoints'] = dict( + blob=self.format_endpoint_dict(account_dict['name'], account_key[0], account_obj.primary_endpoints.blob, 'blob'), + queue=self.format_endpoint_dict(account_dict['name'], account_key[0], account_obj.primary_endpoints.queue, 'queue'), + table=self.format_endpoint_dict(account_dict['name'], account_key[0], account_obj.primary_endpoints.table, 'table') + ) + account_dict['secondary_endpoints'] = None + if account_obj.secondary_endpoints: + account_dict['secondary_endpoints'] = dict( + blob=self.format_endpoint_dict(account_dict['name'], account_key[1], account_obj.primary_endpoints.blob, 'blob'), + queue=self.format_endpoint_dict(account_dict['name'], account_key[1], account_obj.primary_endpoints.queue, 'queue'), + table=self.format_endpoint_dict(account_dict['name'], account_key[1], account_obj.primary_endpoints.table, 'table'), + ) + account_dict['tags'] = None + if account_obj.tags: + account_dict['tags'] = account_obj.tags + blob_service_props = self.get_blob_service_props(account_dict['resource_group'], account_dict['name']) + if blob_service_props and blob_service_props.cors and blob_service_props.cors.cors_rules: + account_dict['blob_cors'] = [dict( + allowed_origins=to_native(x.allowed_origins), + allowed_methods=to_native(x.allowed_methods), + max_age_in_seconds=x.max_age_in_seconds, + exposed_headers=to_native(x.exposed_headers), + allowed_headers=to_native(x.allowed_headers) + ) for x in blob_service_props.cors.cors_rules] + return account_dict + + def format_endpoint_dict(self, name, key, endpoint, storagetype, protocol='https'): + result = dict(endpoint=endpoint) + if key: + result['connectionstring'] = 'DefaultEndpointsProtocol={0};EndpointSuffix={1};AccountName={2};AccountKey={3};{4}Endpoint={5}'.format( + protocol, + self._cloud_environment.suffixes.storage_endpoint, + name, + key, + str.title(storagetype), + endpoint) + return result + + def get_blob_service_props(self, resource_group, name): + if not self.show_blob_cors: + return None + try: + blob_service_props = self.storage_client.blob_services.get_service_properties(resource_group, name) + return blob_service_props + except Exception: + pass + return None + + def get_connectionstring(self, resource_group, name): + keys = ['', ''] + if not self.show_connection_string: + return keys + try: + cred = self.storage_client.storage_accounts.list_keys(resource_group, name) + # get the following try catch from CLI + try: + keys = [cred.keys[0].value, cred.keys[1].value] + except AttributeError: + keys = [cred.key1, cred.key2] + except Exception: + pass + return keys def main(): diff --git a/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine.py b/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine.py index f0f3db20b21..fda2188af26 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine.py @@ -1779,10 +1779,10 @@ class AzureRMVirtualMachine(AzureRMModuleBase): self.log("Storage account {0} found.".format(storage_account_name)) self.check_provisioning_state(account) return account - sku = self.storage_models.Sku(self.storage_models.SkuName.standard_lrs) + sku = self.storage_models.Sku(name=self.storage_models.SkuName.standard_lrs) sku.tier = self.storage_models.SkuTier.standard kind = self.storage_models.Kind.storage - parameters = self.storage_models.StorageAccountCreateParameters(sku, kind, self.location) + parameters = self.storage_models.StorageAccountCreateParameters(sku=sku, kind=kind, location=self.location) self.log("Creating storage account {0} in location {1}".format(storage_account_name, self.location)) self.results['actions'].append("Created storage account {0}".format(storage_account_name)) try: diff --git a/packaging/requirements/requirements-azure.txt b/packaging/requirements/requirements-azure.txt index fbbc2d5d5f1..98cdc388132 100644 --- a/packaging/requirements/requirements-azure.txt +++ b/packaging/requirements/requirements-azure.txt @@ -19,7 +19,7 @@ azure-mgmt-redis==5.0.0 azure-mgmt-resource==1.2.2 azure-mgmt-rdbms==1.4.1 azure-mgmt-sql==0.10.0 -azure-mgmt-storage==1.5.0 +azure-mgmt-storage==3.1.0 azure-mgmt-trafficmanager==0.50.0 azure-mgmt-web==0.32.0 azure-nspkg==2.0.0 diff --git a/test/integration/targets/azure_rm_storageaccount/tasks/main.yml b/test/integration/targets/azure_rm_storageaccount/tasks/main.yml index a315be4a029..463ec76df77 100644 --- a/test/integration/targets/azure_rm_storageaccount/tasks/main.yml +++ b/test/integration/targets/azure_rm_storageaccount/tasks/main.yml @@ -18,6 +18,7 @@ resource_group: "{{ resource_group }}" name: "{{ storage_account }}" state: absent + force_delete_nonempty: True - name: Create new storage account azure_rm_storageaccount: @@ -25,6 +26,19 @@ name: "{{ storage_account }}" account_type: Standard_LRS append_tags: no + blob_cors: + - allowed_origins: + - http://www.example.com/ + allowed_methods: + - GET + - POST + allowed_headers: + - x-ms-meta-data* + - x-ms-meta-target* + - x-ms-meta-abc + exposed_headers: + - x-ms-meta-* + max_age_in_seconds: 200 tags: test: test galaxy: galaxy @@ -35,6 +49,35 @@ that: - output.changed - output.state.id is defined + - output.state.blob_cors | length == 1 + + - name: Create new storage account (idempotence) + azure_rm_storageaccount: + resource_group: "{{ resource_group }}" + name: "{{ storage_account }}" + account_type: Standard_LRS + append_tags: no + blob_cors: + - allowed_origins: + - http://www.example.com/ + allowed_methods: + - GET + - POST + allowed_headers: + - x-ms-meta-data* + - x-ms-meta-target* + - x-ms-meta-abc + exposed_headers: + - x-ms-meta-* + max_age_in_seconds: 200 + tags: + test: test + galaxy: galaxy + register: output + + - assert: + that: + - not output.changed - name: Gather facts by tags azure_rm_storageaccount_facts: @@ -69,22 +112,6 @@ - name: Assert CNAME failure assert: { that: "'custom domain name could not be verified' in change_account['msg']" } - - name: Update account tags - azure_rm_storageaccount: - resource_group: "{{ resource_group }}" - name: "{{ storage_account }}" - append_tags: no - tags: - testing: testing - delete: never - galaxy: 'no' - register: output - - - assert: - that: - - "output.state.tags | length == 3" - - "output.state.tags.galaxy == 'no'" - - name: Update account tags azure_rm_storageaccount: resource_group: "{{ resource_group }}" @@ -105,10 +132,17 @@ azure_rm_storageaccount_facts: resource_group: "{{ resource_group }}" name: "{{ storage_account }}" + show_connection_string: True + show_blob_cors: True - assert: that: - "azure_storageaccounts| length == 1" + - "storageaccounts | length == 1" + - not storageaccounts[0].custom_domain + - storageaccounts[0].account_type == "Standard_GRS" + - storageaccounts[0].primary_endpoints.blob.connectionstring + - storageaccounts[0].blob_cors - name: Gather facts azure_rm_storageaccount_facts: diff --git a/test/integration/targets/azure_rm_virtualmachine_extension/tasks/main.yml b/test/integration/targets/azure_rm_virtualmachine_extension/tasks/main.yml index ce74ecc0240..1a524e4c18c 100644 --- a/test/integration/targets/azure_rm_virtualmachine_extension/tasks/main.yml +++ b/test/integration/targets/azure_rm_virtualmachine_extension/tasks/main.yml @@ -165,6 +165,7 @@ name: "{{ storage_account }}" type: Standard_LRS state: absent + force_delete_nonempty: true - name: Delete Network Security Group that allows SSH azure_rm_securitygroup: diff --git a/test/runner/requirements/integration.cloud.azure.txt b/test/runner/requirements/integration.cloud.azure.txt index fbbc2d5d5f1..98cdc388132 100644 --- a/test/runner/requirements/integration.cloud.azure.txt +++ b/test/runner/requirements/integration.cloud.azure.txt @@ -19,7 +19,7 @@ azure-mgmt-redis==5.0.0 azure-mgmt-resource==1.2.2 azure-mgmt-rdbms==1.4.1 azure-mgmt-sql==0.10.0 -azure-mgmt-storage==1.5.0 +azure-mgmt-storage==3.1.0 azure-mgmt-trafficmanager==0.50.0 azure-mgmt-web==0.32.0 azure-nspkg==2.0.0