diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py index eede56bf774..c51ff09f5c5 100644 --- a/lib/ansible/module_utils/azure_rm_common.py +++ b/lib/ansible/module_utils/azure_rm_common.py @@ -70,7 +70,8 @@ AZURE_API_PROFILES = { 'WebSiteManagementClient': '2018-02-01', 'PostgreSQLManagementClient': '2017-12-01', 'MySQLManagementClient': '2017-12-01', - 'MariaDBManagementClient': '2019-03-01' + 'MariaDBManagementClient': '2019-03-01', + 'ManagementLockClient': '2016-09-01' }, '2017-03-09-profile': { @@ -177,6 +178,7 @@ try: from msrest.service_client import ServiceClient from msrestazure import AzureConfiguration from msrest.authentication import Authentication + from azure.mgmt.resource.locks import ManagementLockClient except ImportError as exc: Authentication = object HAS_AZURE_EXC = traceback.format_exc() @@ -328,6 +330,7 @@ class AzureRMModuleBase(object): self._servicebus_client = None self._automation_client = None self._IoThub_client = None + self._lock_client = None self.check_mode = self.module.check_mode self.api_profile = self.module.params.get('api_profile') @@ -1078,6 +1081,32 @@ class AzureRMModuleBase(object): def IoThub_models(self): return IoTHubModels + @property + def automation_client(self): + self.log('Getting automation client') + if not self._automation_client: + self._automation_client = self.get_mgmt_svc_client(AutomationClient, + base_url=self._cloud_environment.endpoints.resource_manager) + return self._automation_client + + @property + def automation_models(self): + return AutomationModel + + @property + def lock_client(self): + self.log('Getting lock client') + if not self._lock_client: + self._lock_client = self.get_mgmt_svc_client(ManagementLockClient, + base_url=self._cloud_environment.endpoints.resource_manager, + api_version='2016-09-01') + return self._lock_client + + @property + def lock_models(self): + self.log("Getting lock models") + return ManagementLockClient.models('2016-09-01') + class AzureSASAuthentication(Authentication): """Simple SAS Authentication. @@ -1094,17 +1123,6 @@ class AzureSASAuthentication(Authentication): session.headers['Authorization'] = self.token return session - def automation_client(self): - self.log('Getting automation client') - if not self._automation_client: - self._automation_client = self.get_mgmt_svc_client(AutomationClient, - base_url=self._cloud_environment.endpoints.resource_manager) - return self._automation_client - - @property - def automation_models(self): - return AutomationModel - class AzureRMAuthException(Exception): pass diff --git a/lib/ansible/modules/cloud/azure/azure_rm_lock.py b/lib/ansible/modules/cloud/azure/azure_rm_lock.py new file mode 100644 index 00000000000..932a1649f77 --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_lock.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Yuwei Zhou, +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: azure_rm_lock +version_added: "2.9" +short_description: Manage Azure locks +description: + - Create, delete an Azure lock. + - To create or delete management locks, you must have access to Microsoft.Authorization/* or Microsoft.Authorization/locks/* actions. + - Of the built-in roles, only Owner and User Access Administrator are granted those actions. +options: + name: + description: + - Name of the lock. + type: str + required: true + managed_resource_id: + description: + - Manage a lock for the specified resource ID. + - Mututally exclusive with I(resource_group). + - If neither I(managed_resource_id) or I(resource_group) are specified, manage a lock for the current subscription. + - "'/subscriptions/{subscriptionId}' for subscriptions." + - "'/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}' for resource groups." + - "'/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{namespace}/{resourceType}/{resourceName}' for resources." + type: str + resource_group: + description: + - Manage a lock for the named resource group. + - Mutually exclusive with I(managed_resource_id). + - If neither I(managed_resource_id) or I(resource_group) are specified, manage a lock for the current subscription. + type: str + state: + description: + - State of the lock. + - Use C(present) to create or update a lock and C(absent) to delete a lock. + type: str + default: present + choices: + - absent + - present + level: + description: + - The lock level type. + type: str + choices: + - can_not_delete + - read_only +extends_documentation_fragment: + - azure + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Create a lock for a resource + azure_rm_lock: + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM + name: myLock + level: read_only + +- name: Create a lock for a resource group + azure_rm_lock: + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup + name: myLock + level: read_only + +- name: Create a lock for a resource group + azure_rm_lock: + resource_group: myResourceGroup + name: myLock + level: read_only + +- name: Create a lock for a subscription + azure_rm_lock: + name: myLock + level: read_only +''' + +RETURN = ''' +id: + description: + - Resource ID of the lock. + returned: success + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Authorization/locks/keep" +''' # NOQA + +from ansible.module_utils.azure_rm_common import AzureRMModuleBase + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMLock(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + resource_group=dict(type='str'), + managed_resource_id=dict(type='str'), + level=dict(type='str', choices=['can_not_delete', 'read_only']) + ) + + self.results = dict( + changed=False, + id=None + ) + + required_if = [ + ('state', 'present', ['level']) + ] + + mutually_exclusive = [['resource_group', 'managed_resource_id']] + + self.name = None + self.state = None + self.level = None + self.resource_group = None + self.managed_resource_id = None + + super(AzureRMLock, self).__init__(self.module_arg_spec, + supports_check_mode=True, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_tags=False) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + changed = False + # construct scope id + scope = self.get_scope() + lock = self.get_lock(scope) + if self.state == 'present': + lock_level = getattr(self.lock_models.LockLevel, self.level) + if not lock: + changed = True + lock = self.lock_models.ManagementLockObject(level=lock_level) + elif lock.level != lock_level: + self.log('Lock level changed') + lock.level = lock_level + changed = True + if not self.check_mode: + lock = self.create_or_update_lock(scope, lock) + self.results['id'] = lock.id + elif lock: + changed = True + if not self.check_mode: + self.delete_lock(scope) + self.results['changed'] = changed + return self.results + + def delete_lock(self, scope): + try: + return self.lock_client.management_locks.delete_by_scope(scope, self.name) + except CloudError as exc: + self.fail('Error when deleting lock {0} for {1}: {2}'.format(self.name, scope, exc.message)) + + def create_or_update_lock(self, scope, lock): + try: + return self.lock_client.management_locks.create_or_update_by_scope(scope, self.name, lock) + except CloudError as exc: + self.fail('Error when creating or updating lock {0} for {1}: {2}'.format(self.name, scope, exc.message)) + + def get_lock(self, scope): + try: + return self.lock_client.management_locks.get_by_scope(scope, self.name) + except CloudError as exc: + if exc.status_code in [404]: + return None + self.fail('Error when getting lock {0} for {1}: {2}'.format(self.name, scope, exc.message)) + + def get_scope(self): + ''' + Get the resource scope of the lock management. + '/subscriptions/{subscriptionId}' for subscriptions, + '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}' for resource groups, + '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{namespace}/{resourceType}/{resourceName}' for resources. + ''' + if self.managed_resource_id: + return self.managed_resource_id + elif self.resource_group: + return '/subscriptions/{0}/resourcegroups/{1}'.format(self.subscription_id, self.resource_group) + else: + return '/subscriptions/{0}'.format(self.subscription_id) + + +def main(): + AzureRMLock() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/azure/azure_rm_lock_facts.py b/lib/ansible/modules/cloud/azure/azure_rm_lock_facts.py new file mode 100644 index 00000000000..e6ac7bb408c --- /dev/null +++ b/lib/ansible/modules/cloud/azure/azure_rm_lock_facts.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +# +# Copyright (c) 2019 Yuwei Zhou, +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: azure_rm_lock_facts +version_added: "2.9" +short_description: Manage Azure locks +description: + - Create, delete an Azure lock. +options: + name: + description: + - Name of the lock. + type: str + required: true + managed_resource_id: + description: + - ID of the resource where need to manage the lock. + - Get this via facts module. + - Cannot be set mutal with I(resource_group). + - Manage subscription if both I(managed_resource_id) and I(resource_group) not defined. + - "'/subscriptions/{subscriptionId}' for subscriptions." + - "'/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}' for resource groups." + - "'/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{namespace}/{resourceType}/{resourceName}' for resources." + - Can get all locks with 'child scope' for this resource, use I(managed_resource_id) in response for further management. + type: str + resource_group: + description: + - Resource group name where need to manage the lock. + - The lock is in the resource group level. + - Cannot be set mutal with I(managed_resource_id). + - Query subscription if both I(managed_resource_id) and I(resource_group) not defined. + - Can get all locks with 'child scope' in this resource group, use the I(managed_resource_id) in response for further management. + type: str + +extends_documentation_fragment: + - azure + +author: + - Yuwei Zhou (@yuwzho) + +''' + +EXAMPLES = ''' +- name: Get myLock details of myVM + azure_rm_lock_facts: + name: myLock + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM + +- name: List locks of myVM + azure_rm_lock_facts: + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM + +- name: List locks of myResourceGroup + azure_rm_lock_facts: + resource_group: myResourceGroup + +- name: List locks of myResourceGroup + azure_rm_lock_facts: + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup + +- name: List locks of mySubscription + azure_rm_lock_facts: + +- name: List locks of mySubscription + azure_rm_lock_facts: + managed_resource_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +''' + +RETURN = ''' +locks: + description: + - List of locks dicts. + returned: always + type: complex + contains: + id: + description: + - ID of the Lock. + returned: always + type: str + sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Authorization/locks/myLock" + name: + description: + - Name of the lock. + returned: always + type: str + sample: myLock + level: + description: + - Type level of the lock. + returned: always + type: str + sample: can_not_delete + notes: + description: + - Notes of the lock added by creator. + returned: always + type: str + sample: "This is a lock" +''' # NOQA + +import json +import re +from ansible.module_utils.common.dict_transformations import _camel_to_snake +from ansible.module_utils.azure_rm_common import AzureRMModuleBase +from ansible.module_utils.azure_rm_common_rest import GenericRestClient + +try: + from msrestazure.azure_exceptions import CloudError +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMLockFacts(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + name=dict(type='str'), + resource_group=dict(type='str'), + managed_resource_id=dict(type='str') + ) + + self.results = dict( + changed=False, + locks=[] + ) + + mutually_exclusive = [['resource_group', 'managed_resource_id']] + + self.name = None + self.resource_group = None + self.managed_resource_id = None + self._mgmt_client = None + self._query_parameters = {'api-version': '2016-09-01'} + self._header_parameters = {'Content-Type': 'application/json; charset=utf-8'} + + super(AzureRMLockFacts, self).__init__(self.module_arg_spec, facts_module=True, mutually_exclusive=mutually_exclusive, supports_tags=False) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys(): + setattr(self, key, kwargs[key]) + + self._mgmt_client = self.get_mgmt_svc_client(GenericRestClient, base_url=self._cloud_environment.endpoints.resource_manager) + changed = False + # construct scope id + scope = self.get_scope() + url = '/{0}/providers/Microsoft.Authorization/locks'.format(scope) + if self.name: + url = '{0}/{1}'.format(url, self.name) + locks = self.list_locks(url) + resp = locks.get('value') if 'value' in locks else [locks] + self.results['locks'] = [self.to_dict(x) for x in resp] + return self.results + + def to_dict(self, lock): + resp = dict( + id=lock['id'], + name=lock['name'], + level=_camel_to_snake(lock['properties']['level']), + managed_resource_id=re.sub('/providers/Microsoft.Authorization/locks/.+', '', lock['id']) + ) + if lock['properties'].get('notes'): + resp['notes'] = lock['properties']['notes'] + if lock['properties'].get('owners'): + resp['owners'] = [x['application_id'] for x in lock['properties']['owners']] + return resp + + def list_locks(self, url): + try: + resp = self._mgmt_client.query(url=url, + method='GET', + query_parameters=self._query_parameters, + header_parameters=self._header_parameters, + body=None, + expected_status_codes=[200], + polling_timeout=None, + polling_interval=None) + return json.loads(resp.text) + except CloudError as exc: + self.fail('Error when finding locks {0}: {1}'.format(url, exc.message)) + + def get_scope(self): + ''' + Get the resource scope of the lock management. + '/subscriptions/{subscriptionId}' for subscriptions, + '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}' for resource groups, + '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{namespace}/{resourceType}/{resourceName}' for resources. + ''' + if self.managed_resource_id: + return self.managed_resource_id + elif self.resource_group: + return '/subscriptions/{0}/resourcegroups/{1}'.format(self.subscription_id, self.resource_group) + else: + return '/subscriptions/{0}'.format(self.subscription_id) + + +def main(): + AzureRMLockFacts() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/azure_rm_lock/aliases b/test/integration/targets/azure_rm_lock/aliases new file mode 100644 index 00000000000..b13b9e8a575 --- /dev/null +++ b/test/integration/targets/azure_rm_lock/aliases @@ -0,0 +1,4 @@ +cloud/azure +destructive +unsupported +azure_rm_lock_facts diff --git a/test/integration/targets/azure_rm_lock/meta/main.yml b/test/integration/targets/azure_rm_lock/meta/main.yml new file mode 100644 index 00000000000..95e1952f989 --- /dev/null +++ b/test/integration/targets/azure_rm_lock/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_azure diff --git a/test/integration/targets/azure_rm_lock/tasks/main.yml b/test/integration/targets/azure_rm_lock/tasks/main.yml new file mode 100644 index 00000000000..9deb7d2dda1 --- /dev/null +++ b/test/integration/targets/azure_rm_lock/tasks/main.yml @@ -0,0 +1,99 @@ +- name: Create a virtual network + azure_rm_virtualnetwork: + name: mytestvirtualnetworklock + resource_group: "{{ resource_group }}" + address_prefixes_cidr: + - "10.1.0.0/16" + register: vn + +- name: Add lock to resource (check_mode) + azure_rm_lock: + name: keep + managed_resource_id: "{{ vn.state.id }}" + level: read_only + register: lock + check_mode: yes + +- assert: + that: + - lock.changed + +- name: Query lock + azure_rm_lock_facts: + managed_resource_id: "{{ vn.state.id }}" + register: locks + +- assert: + that: + - locks.locks | length == 0 + +- name: Add lock to resource + azure_rm_lock: + name: keep + managed_resource_id: "{{ vn.state.id }}" + level: read_only + register: lock + +- assert: + that: + - lock.changed + - lock.id + +- name: Query lock + azure_rm_lock_facts: + name: keep + managed_resource_id: "{{ vn.state.id }}" + register: locks + +- assert: + that: + - locks.locks | length == 1 + +- name: Update lock to resource (idempontent) + azure_rm_lock: + name: keep + managed_resource_id: "{{ vn.state.id }}" + level: read_only + register: lock1 + +- assert: + that: + - not lock1.changed + - lock1.id == lock.id + +- name: Update lock level + azure_rm_lock: + name: keep + managed_resource_id: "{{ vn.state.id }}" + level: can_not_delete + register: lock + +- assert: + that: + - lock.changed + - lock.level == 'can_not_delete' + +- name: Delete lock + azure_rm_lock: + name: keep + managed_resource_id: "{{ vn.state.id }}" + register: lock + +- assert: + that: + - lock.changed + +- name: Query lock + azure_rm_lock_facts: + managed_resource_id: "{{ vn.state.id }}" + register: locks + +- assert: + that: + - locks.locks | length == 0 + +- name: Clean up + azure_rm_virtualnetwork: + name: mytestvirtualnetworklock + resource_group: "{{ resource_group }}" + state: absent \ No newline at end of file