Skip to content

Commit

Permalink
Identity storageaccount (#1639)
Browse files Browse the repository at this point in the history
* Fix update_managed_identity to handle API that uses PATCH RESTAPI

Some API endpoints use PATCH Rest API and will append UserAssignedIdentities
without removing the original ones.  To remove the original entries you must
specify None for the value of the key you want to remove.

* Add Managed Identity support to StorageAccount
  • Loading branch information
p3ck authored Jul 23, 2024
1 parent 6441529 commit e5ea1ed
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 17 deletions.
28 changes: 19 additions & 9 deletions plugins/module_utils/azure_rm_common_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def default_compare(self, modifiers, new, old, path, result):
else:
return True

def update_single_managed_identity(self, curr_identity, new_identity):
def update_single_managed_identity(self, curr_identity, new_identity, patch_support=False):
# Converting from single_managed_identity_spec to managed_identity_spec
new_identity = new_identity or dict()
new_identity_converted = {
Expand All @@ -268,9 +268,12 @@ def update_single_managed_identity(self, curr_identity, new_identity):
new_identity_converted['user_assigned_identities'] = {
'id': [user_assigned_identity]
}
return self.update_managed_identity(curr_identity=curr_identity, new_identity=new_identity_converted, allow_identities_append=False)
return self.update_managed_identity(curr_identity=curr_identity,
new_identity=new_identity_converted,
allow_identities_append=False, patch_support=patch_support)

def update_managed_identity(self, curr_identity=None, new_identity=None, allow_identities_append=True):
def update_managed_identity(self, curr_identity=None, new_identity=None,
allow_identities_append=True, patch_support=False):
curr_identity = curr_identity or dict()
# TODO need to remove self.module.params.get('identity', {})
# after changing all modules to provide the "new_identity" parameter
Expand All @@ -289,25 +292,32 @@ def update_managed_identity(self, curr_identity=None, new_identity=None, allow_i
if new_managed_type == 'None' or curr_managed_type != new_managed_type:
changed = True

curr_user_assigned_identities = set(curr_identity.get('user_assigned_identities', {}).keys())
new_user_assigned_identities = set(new_identity.get('user_assigned_identities', {}).get('id', []))
curr_user_assigned_identities = set((curr_identity.get('user_assigned_identities') or {}).keys())
new_user_assigned_identities = set((new_identity.get('user_assigned_identities') or {}).get('id', []))
result_user_assigned_identities = new_user_assigned_identities
clear_identities = []

result_identity = self.managed_identity['identity'](type=new_managed_type)

# If type in module args contains 'UserAssigned'
if allow_identities_append and \
'UserAssigned' in new_managed_type and \
new_identity.get('user_assigned_identities', {}).get('append', True) is True:
result_user_assigned_identities = new_user_assigned_identities.union(curr_user_assigned_identities)
elif patch_support and 'UserAssigned' in new_managed_type:
clear_identities = curr_user_assigned_identities.difference(result_user_assigned_identities)

# Check if module args identities are different as current ones
if result_user_assigned_identities.difference(curr_user_assigned_identities) != set():
changed = True

result_identity = self.managed_identity['identity'](type=new_managed_type)

# Append identities to the model
if len(result_user_assigned_identities) > 0:
# Update User Assigned identities to the model
if len(result_user_assigned_identities) > 0 or len(clear_identities) > 0:
result_identity.user_assigned_identities = {}
# Set identity to None to remove it
for identity in clear_identities:
result_identity.user_assigned_identities[identity] = None
# Set identity to user_assigned to add it
for identity in result_user_assigned_identities:
result_identity.user_assigned_identities[identity] = self.managed_identity['user_assigned']()

Expand Down
76 changes: 72 additions & 4 deletions plugins/modules/azure_rm_storageaccount.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,26 @@
description:
- A boolean indicating whether or not the service applies a secondary layer of encryption with platform managed keys for data at rest.
type: bool
identity:
description:
- Identity for this resource.
type: dict
version_added: '2.7.0'
suboptions:
type:
description:
- Type of the managed identity
choices:
- SystemAssigned
- UserAssigned
- 'None'
default: 'None'
type: str
user_assigned_identity:
description:
- User Assigned Managed Identity associated to this resource
required: false
type: str
extends_documentation_fragment:
- azure.azcollection.azure
Expand Down Expand Up @@ -695,9 +715,17 @@


import copy
from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AZURE_SUCCESS_STATE, AzureRMModuleBase
import time
from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AZURE_SUCCESS_STATE
from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt
from ansible.module_utils._text import to_native

try:
from azure.mgmt.storage.models import (Identity, UserAssignedIdentity)
except ImportError:
# This is handled in azure_rm_common
pass

cors_rule_spec = dict(
allowed_origins=dict(type='list', elements='str', required=True),
allowed_methods=dict(type='list', elements='str', required=True),
Expand Down Expand Up @@ -752,7 +780,7 @@ def compare_cors(cors1, cors2):
return True


class AzureRMStorageAccount(AzureRMModuleBase):
class AzureRMStorageAccount(AzureRMModuleBaseExt):

def __init__(self):

Expand Down Expand Up @@ -810,7 +838,11 @@ def __init__(self):
require_infrastructure_encryption=dict(type='bool'),
key_source=dict(type='str', choices=["Microsoft.Storage", "Microsoft.Keyvault"], default='Microsoft.Storage')
)
)
),
identity=dict(
type="dict",
options=self.managed_identity_single_spec
),
)

self.results = dict(
Expand Down Expand Up @@ -843,10 +875,22 @@ def __init__(self):
self.allow_shared_key_access = None
self.allow_cross_tenant_replication = None
self.default_to_o_auth_authentication = None
self._managed_identity = None
self.identity = None
self.update_identity = False

super(AzureRMStorageAccount, self).__init__(self.module_arg_spec,
supports_check_mode=True)

@property
def managed_identity(self):
if not self._managed_identity:
self._managed_identity = {
"identity": Identity,
"user_assigned": UserAssignedIdentity,
}
return self._managed_identity

def exec_module(self, **kwargs):

for key in list(self.module_arg_spec.keys()) + ['tags']:
Expand All @@ -871,6 +915,14 @@ def exec_module(self, **kwargs):
self.fail("Parameter error: Storage account with {0} kind require account type is Premium_LRS or Premium_ZRS".format(self.kind))
self.account_dict = self.get_account()

curr_identity = self.account_dict["identity"] if self.account_dict else None

if self.identity:
self.update_identity, identity_result = self.update_single_managed_identity(curr_identity=curr_identity,
new_identity=self.identity,
patch_support=True)
self.identity = identity_result.as_dict()

if self.state == 'present' and self.account_dict and \
self.account_dict['provisioning_state'] != AZURE_SUCCESS_STATE:
self.fail("Error: storage account {0} has not completed provisioning. State is {1}. Expecting state "
Expand All @@ -885,7 +937,7 @@ def exec_module(self, **kwargs):
if not self.account_dict:
self.results['state'] = self.create_account()
else:
self.update_account()
self.results['state'] = self.update_account()
elif self.state == 'absent' and self.account_dict:
self.delete_account()
self.results['state'] = dict(Status='Deleted')
Expand Down Expand Up @@ -1029,6 +1081,10 @@ def account_obj_to_dict(self, account_obj, blob_mgmt_props=None, blob_client_pro
if account_obj.encryption.services.blob:
account_dict['encryption']['services']['blob'] = dict(enabled=True)

account_dict['identity'] = dict()
if account_obj.identity:
account_dict['identity'] = account_obj.identity.as_dict()

return account_dict

def failover_account(self):
Expand Down Expand Up @@ -1240,6 +1296,14 @@ def update_account(self):
except Exception as exc:
self.fail("Failed to update custom domain: {0}".format(str(exc)))

if self.update_identity:
self.results['changed'] = True
parameters = self.storage_models.StorageAccountUpdateParameters(identity=self.identity)
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)))

if self.access_tier:
if not self.account_dict['access_tier'] or self.account_dict['access_tier'] != self.access_tier:
self.results['changed'] = True
Expand Down Expand Up @@ -1308,6 +1372,8 @@ def update_account(self):

if encryption_changed and not self.check_mode:
self.fail("The encryption can't update encryption, encryption info as {0}".format(self.account_dict['encryption']))
time.sleep(1)
return self.get_account()

def create_account(self):
self.log("Creating account {0}".format(self.name))
Expand Down Expand Up @@ -1341,6 +1407,7 @@ def create_account(self):
default_to_o_auth_authentication=self.default_to_o_auth_authentication,
allow_cross_tenant_replication=self.allow_cross_tenant_replication,
allow_shared_key_access=self.allow_shared_key_access,
identity=self.identity,
tags=dict()
)
if self.tags:
Expand All @@ -1360,6 +1427,7 @@ def create_account(self):
kind=self.kind,
location=self.location,
tags=self.tags,
identity=self.identity,
enable_https_traffic_only=self.https_only,
minimum_tls_version=self.minimum_tls_version,
public_network_access=self.public_network_access,
Expand Down
5 changes: 5 additions & 0 deletions plugins/modules/azure_rm_storageaccount_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,11 @@ def account_obj_to_dict(self, account_obj):
account_dict['encryption']['services']['queue'] = dict(enabled=True)
if account_obj.encryption.services.blob:
account_dict['encryption']['services']['blob'] = dict(enabled=True)

account_dict['identity'] = dict()
if account_obj.identity:
account_dict['identity'] = account_obj.identity.as_dict()

return account_dict

def format_endpoint_dict(self, name, key, endpoint, storagetype, protocol='https'):
Expand Down
33 changes: 30 additions & 3 deletions tests/integration/targets/azure_rm_iothub/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
user_assigned_identities:
id:
- "{{ user_identity_1 }}"
- "{{ user_identity_2 }}"
register: iothub

- name: Assert tht iot hub created
Expand All @@ -93,7 +92,6 @@
that:
- iothub.iothubs[0].identity.type == 'UserAssigned'
- user_identity_1 in iothub.iothubs[0].identity.user_assigned_identities
- user_identity_2 in iothub.iothubs[0].identity.user_assigned_identities

- name: Create IoT Hub (idempontent)
azure_rm_iothub:
Expand All @@ -108,14 +106,43 @@
user_assigned_identities:
id:
- "{{ user_identity_1 }}"
- "{{ user_identity_2 }}"
register: iothub

- name: Assert the iot hub created
ansible.builtin.assert:
that:
- not iothub.changed

- name: Update IoT Hub (identity)
azure_rm_iothub:
name: "hub{{ rpfx }}"
resource_group: "{{ resource_group }}"
identity:
type: UserAssigned
user_assigned_identities:
id:
- "{{ user_identity_2 }}"
append: false
register: iothub

- name: Assert tht iot hub created
ansible.builtin.assert:
that:
- iothub.changed

- name: Get the IoT Hub facts
azure_rm_iothub_info:
name: "hub{{ rpfx }}"
resource_group: "{{ resource_group }}"
register: iothub

- name: Assert the IoT Hub facts
ansible.builtin.assert:
that:
- iothub.iothubs[0].identity.type == 'UserAssigned'
- iothub.iothubs[0].identity.user_assigned_identities | length == 1
- user_identity_2 in iothub.iothubs[0].identity.user_assigned_identities

- name: Query IoT Hub
azure_rm_iothub_info:
name: "hub{{ rpfx }}"
Expand Down
Loading

0 comments on commit e5ea1ed

Please sign in to comment.