Repository: ansible/ansible

Issue:
azure_rm_storageaccount throws InvalidAccessTier when creating BlobStorage account

##### ISSUE TYPE
Bug Report

##### COMPONENT NAME
azure_rm_storageaccount

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.4.0
  config file = /###/###/###/###/###/###/ansible/ansible.cfg
  configured module search path = [u'/Users/###/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.13 (default, Apr 23 2017, 16:50:35) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
[defaults]

hostfile = ./inventory
inventory = ./inventory
roles_path = ../../../analytics-monitoring/roles:./roles
filter_plugins = ../../../analytics-monitoring/filter_plugins:./filter_plugins
callback_plugins = ../../../analytics-monitoring/callback_plugins:./callback_plugins


[ssh_connection]
pipelining=True

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
OS X 10.12.4 (16E195)

##### SUMMARY
Attempting to create a BlobStorage account results in an InvalidAccessTier:

> msrestazure.azure_exceptions.CloudError: Azure Error: InvalidAccessTier
Message: The storage account property 'accessTier' is invalid or cannot be set at this time.

When creating via the Azure Portal, the default access tier is "Hot". 

The Azure Python SDK has the possible values as Hot, Cool. 

https://github.com/Azure/azure-sdk-for-python/blob/master/azure-mgmt-storage/azure/mgmt/storage/v2016_12_01/models/storage_account_create_parameters.py#L49

The Azure Python SDK allows the creation of StorageAccountCreateParameters with an access tier using:
`def __init__(self, sku, kind, location, tags=None, custom_domain=None, encryption=None, access_tier=None):`

The azure_rm_storageaccount module doesn't accept an access tier nor does it supply a default one:
https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py#L398


##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
    - name: Create resource group
      azure_rm_resourcegroup:
        name: mygroup
        location: southcentralus

    - name: Create standard lrs blob storage account
      azure_rm_storageaccount:
        resource_group: mygroup
        name: mystorageaccount
        location: southcentralus
        account_type: Standard_LRS
        kind: BlobStorage
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
The storage account mystorageaccount would be created in the resource group mygroup

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
ansible-playbook 2.4.0
  config file = /ansible/ansible.cfg
  configured module search path = [u'/Users/####/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible-playbook
  python version = 2.7.13 (default, Apr 23 2017, 16:50:35) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
Using /ansible/ansible.cfg as config file
setting up inventory plugins
Parsed /ansible/inventory/aq3.sh inventory source with script plugin
Set default localhost to localhost
Parsed /ansible/inventory/localhost.hosts inventory source with ini plugin
Parsed /ansible/inventory/optional.sh inventory source with script plugin
 [WARNING]: Found both group and host with same name: localhost

Loading callback plugin default of type stdout, v2.0 from /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc
Loading callback plugin unnamed of type old, v1.0 from /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc
 ___________________________
< PLAYBOOK: copy_images.yml >
 ---------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

1 plays in copy_images.yml
 __________________
< PLAY [localhost] >
 ------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

2017-06-20T22:18:21.655381 (delta: 0.05648)         elapsed: 0.05648 **********
===============================================================================
META: ran handlers
 ____________________________________________________________
< TASK [Create resource group] >
 ------------------------------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

task path: /ansible/copy_images.yml:21
2017-06-20T22:18:21.713703 (delta: 0.058203)         elapsed: 0.114802 ********
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/basic.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/azure_rm_common.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/_text.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/pycompat24.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/six/__init__.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/six/_six.py
Using module file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/modules/cloud/azure/azure_rm_resourcegroup.py
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: ####
<localhost> EXEC /bin/sh -c '/opt/local/bin/python && sleep 0'
ok: [localhost] => {
    "changed": false,
    "contains_resources": true,
    "failed": false,
    "invocation": {
        "module_args": {
            "ad_user": null,
            "append_tags": true,
            "client_id": null,
            "force": false,
            "location": "southcentralus",
            "name": "mygroup",
            "password": null,
            "profile": null,
            "secret": null,
            "state": "present",
            "subscription_id": null,
            "tags": null,
            "tenant": null
        }
    },
    "state": {
        "id": "/subscriptions/###########################/resourceGroups/mygroup",
        "location": "southcentralus",
        "name": "mygroup",
        "provisioning_state": "Succeeded",
        "tags": {}
    }
}
 _________________________________________________
< TASK [Create standard lrs blob storage account] >
 -------------------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

task path: /ansible/copy_images.yml:30
2017-06-20T22:18:23.173263 (delta: 0.050252)         elapsed: 1.574362 ********
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/basic.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/azure_rm_common.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/_text.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/pycompat24.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/six/__init__.py
Using module_utils file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/module_utils/six/_six.py
Using module file /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ansible/modules/cloud/azure/azure_rm_storageaccount.py
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: ####
<localhost> EXEC /bin/sh -c '/opt/local/bin/python && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py", line 447, in <module>
    main()
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py", line 444, in main
    AzureRMStorageAccount()
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py", line 205, in __init__
    supports_check_mode=True)
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_modlib.zip/ansible/module_utils/azure_rm_common.py", line 197, in __init__
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py", line 241, in exec_module
    self.results['state'] = self.create_account()
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py", line 402, in create_account
    self.get_poller_result(poller)
  File "/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_modlib.zip/ansible/module_utils/azure_rm_common.py", line 411, in get_poller_result
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/msrestazure/azure_operation.py", line 486, in wait
    raise self._exception
msrestazure.azure_exceptions.CloudError: Azure Error: InvalidAccessTier
Message: The storage account property 'accessTier' is invalid or cannot be set at this time.

fatal: [localhost]: FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "Traceback (most recent call last):\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py\", line 447, in <module>\n    main()\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py\", line 444, in main\n    AzureRMStorageAccount()\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py\", line 205, in __init__\n    supports_check_mode=True)\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_modlib.zip/ansible/module_utils/azure_rm_common.py\", line 197, in __init__\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py\", line 241, in exec_module\n    self.results['state'] = self.create_account()\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_module_azure_rm_storageaccount.py\", line 402, in create_account\n    self.get_poller_result(poller)\n  File \"/var/folders/mh/twpnm4s11q1d2mp23yx137t00000gp/T/ansible_vKyn6G/ansible_modlib.zip/ansible/module_utils/azure_rm_common.py\", line 411, in get_poller_result\n  File \"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/msrestazure/azure_operation.py\", line 486, in wait\n    raise self._exception\nmsrestazure.azure_exceptions.CloudError: Azure Error: InvalidAccessTier\nMessage: The storage account property 'accessTier' is invalid or cannot be set at this time.\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE",
    "rc": 1
}
	to retry, use: --limit @/ansible/copy_images.retry
 ____________
< PLAY RECAP >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

localhost                  : ok=2    changed=0    unreachable=0    failed=1

2017-06-20T22:18:29.641144 (delta: 6.467837)         elapsed: 8.042243 ********
===============================================================================
```



Discussion:
cc @haroldwongms @tstringer @nitzmahone
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py
--- a/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py
+++ b/lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py
@@ -88,6 +88,15 @@
             - Storage
             - StorageBlob
         version_added: "2.2"
+    access_tier:
+        description:
+            - The access tier for this storage account. Required for a storage account of kind 'BlobStorage'.
+        required: false
+        default: 'Storage'
+        choices:
+            - Hot
+            - Cool
+        version_added: "2.4"
 
 extends_documentation_fragment:
     - azure
@@ -179,7 +188,8 @@ def __init__(self):
             state=dict(default='present', choices=['present', 'absent']),
             force=dict(type='bool', default=False),
             tags=dict(type='dict'),
-            kind=dict(type='str', default='Storage', choices=['Storage', 'BlobStorage'])
+            kind=dict(type='str', default='Storage', choices=['Storage', 'BlobStorage']),
+            access_tier=dict(type='str', choices=['Hot', 'Cool'])
         )
 
         for key in SkuName:
@@ -200,6 +210,7 @@ def __init__(self):
         self.tags = None
         self.force = None
         self.kind = None
+        self.access_tier = None
 
         super(AzureRMStorageAccount, self).__init__(self.module_arg_spec,
                                                     supports_check_mode=True)
@@ -280,6 +291,8 @@ def account_obj_to_dict(self, account_obj):
             location=account_obj.location,
             resource_group=self.resource_group,
             type=account_obj.type,
+            access_tier=(account_obj.access_tier.value
+                        if account_obj.access_tier is not None else None),
             sku_tier=account_obj.sku.tier.value,
             sku_name=account_obj.sku.name.value,
             provisioning_state=account_obj.provisioning_state.value,
@@ -346,8 +359,7 @@ def update_account(self):
                         self.fail("Failed to update account type: {0}".format(str(exc)))
 
         if self.custom_domain:
-            if not self.account_dict['custom_domain'] or \
-               self.account_dict['custom_domain'] != self.account_dict['custom_domain']:
+            if not self.account_dict['custom_domain'] or self.account_dict['custom_domain'] != self.custom_domain:
                 self.results['changed'] = True
                 self.account_dict['custom_domain'] = self.custom_domain
 
@@ -360,6 +372,18 @@ def update_account(self):
                 except Exception as exc:
                     self.fail("Failed to update custom domain: {0}".format(str(exc)))
 
+        if self.access_tier:
+            if not self.account_dict['access_tier'] or self.account_dict['access_tier'] != self.access_tier:
+                self.results['changed'] = True
+                self.account_dict['access_tier'] = self.access_tier
+
+            if self.results['changed'] and not self.check_mode:
+                parameters = StorageAccountUpdateParameters(access_tier=self.access_tier)
+                try:
+                    self.storage_client.storage_accounts.update(self.resource_group, self.name, parameters)
+                except Exception as exc:
+                    self.fail("Failed to update access tier: {0}".format(str(exc)))
+
         update_tags, self.account_dict['tags'] = self.update_tags(self.account_dict['tags'])
         if update_tags:
             self.results['changed'] = True
@@ -379,6 +403,9 @@ def create_account(self):
         if not self.account_type:
             self.fail('Parameter error: account_type required when creating a storage account.')
 
+        if not self.access_tier and self.kind == 'BlobStorage':
+            self.fail('Parameter error: access_tier required when creating a storage account of type BlobStorage.')
+
         self.check_name_availability()
         self.results['changed'] = True
 
@@ -395,7 +422,7 @@ def create_account(self):
             return account_dict
         sku = Sku(SkuName(self.account_type))
         sku.tier = SkuTier.standard if 'Standard' in self.account_type else SkuTier.premium
-        parameters = StorageAccountCreateParameters(sku, self.kind, self.location, tags=self.tags)
+        parameters = StorageAccountCreateParameters(sku, self.kind, 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)


Test patch: