diff --git a/.gitignore b/.gitignore index a5e318764..f92aa6737 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ venv* .vscode ansible_collections/ .idea/ +tests/integration/inventory diff --git a/plugins/modules/azure_rm_virtualmachine.py b/plugins/modules/azure_rm_virtualmachine.py index 83a8d412d..1873f50b8 100644 --- a/plugins/modules/azure_rm_virtualmachine.py +++ b/plugins/modules/azure_rm_virtualmachine.py @@ -229,6 +229,10 @@ description: - Size of OS disk in GB. type: int + os_disk_encryption_set: + description: + - ID of disk encryption set for OS disk. + type: str os_type: description: - Base type of operating system. @@ -1088,6 +1092,7 @@ def __init__(self): storage_blob_name=dict(type='str', aliases=['storage_blob']), os_disk_caching=dict(type='str', aliases=['disk_caching'], choices=['ReadOnly', 'ReadWrite']), os_disk_size_gb=dict(type='int'), + os_disk_encryption_set=dict(type='str'), managed_disk_type=dict(type='str', choices=['Standard_LRS', 'StandardSSD_LRS', 'StandardSSD_ZRS', 'Premium_LRS', 'Premium_ZRS', 'UltraSSD_LRS']), os_disk_name=dict(type='str'), proximity_placement_group=dict(type='dict', options=proximity_placement_group_spec), @@ -1172,6 +1177,7 @@ def __init__(self): self.os_type = None self.os_disk_caching = None self.os_disk_size_gb = None + self.os_disk_encryption_set = None self.managed_disk_type = None self.os_disk_name = None self.proximity_placement_group = None @@ -1209,8 +1215,10 @@ def __init__(self): ansible_facts=dict(azure_vm=None) ) + required_if = [('os_disk_encryption_set', '*', ['managed_disk_type'])] + super(AzureRMVirtualMachine, self).__init__(derived_arg_spec=self.module_arg_spec, - supports_check_mode=True) + supports_check_mode=True, required_if=required_if) @property def boot_diagnostics_present(self): @@ -1559,6 +1567,11 @@ def exec_module(self, **kwargs): vm_dict['os_profile']['linux_configuration']['disable_password_authentication']: self.fail("(PropertyChangeNotAllowed) Changing property 'linuxConfiguration.disablePasswordAuthentication' is not allowed.") + current_os_des_id = vm_dict['storage_profile'].get('os_disk', {}).get('managed_disk', {}).get('disk_encryption_set', {}).get('id', None) + if self.os_disk_encryption_set is not None and current_os_des_id is not None: + if self.os_disk_encryption_set != current_os_des_id: + self.fail("(PropertyChangeNotAllowed) Changing property 'storage_profile.os_disk.managed_disk.disk_encryption_set' is not allowed.") + # Defaults for boot diagnostics if 'diagnostics_profile' not in vm_dict: vm_dict['diagnostics_profile'] = {} @@ -1700,6 +1713,11 @@ def exec_module(self, **kwargs): vhd = self.compute_models.VirtualHardDisk(uri=requested_vhd_uri) managed_disk = None + if managed_disk and self.os_disk_encryption_set: + managed_disk.disk_encryption_set = self.compute_models.DiskEncryptionSetParameters( + id=self.os_disk_encryption_set + ) + plan = None if self.plan: plan = self.compute_models.Plan(name=self.plan.get('name'), product=self.plan.get('product'), diff --git a/tests/integration/targets/azure_rm_virtualmachine/inventory.yml b/tests/integration/targets/azure_rm_virtualmachine/inventory.yml index 88172852a..63ba77727 100644 --- a/tests/integration/targets/azure_rm_virtualmachine/inventory.yml +++ b/tests/integration/targets/azure_rm_virtualmachine/inventory.yml @@ -40,6 +40,10 @@ all: network: 10.42.7.0/24 subnet: 10.42.7.0/28 + azure_test_encrypted: + network: 10.42.8.0/24 + subnet: 10.42.8.0/28 + vars: ansible_connection: local ansible_python_interpreter: "{{ ansible_playbook_python }}" @@ -55,6 +59,7 @@ all: security_group: "{{ 'sg' ~ uid_short }}" public_ip_name: "{{ 'ip' ~ uid_short }}" interface_name: "{{ 'int' ~ uid_short }}" + des_name: "{{ 'des' ~ uid_short }}" ssh_keys: - path: '/home/chouseknecht/.ssh/authorized_keys' diff --git a/tests/integration/targets/azure_rm_virtualmachine/lookup_plugins/azure_service_principal_attribute.py b/tests/integration/targets/azure_rm_virtualmachine/lookup_plugins/azure_service_principal_attribute.py new file mode 100644 index 000000000..c6f488f13 --- /dev/null +++ b/tests/integration/targets/azure_rm_virtualmachine/lookup_plugins/azure_service_principal_attribute.py @@ -0,0 +1,92 @@ +# (c) 2018 Yunge Zhu, +# (c) 2017 Ansible Project +# 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 + +DOCUMENTATION = """ +lookup: azure_service_principal_attribute + +requirements: + - azure-graphrbac + +author: + - Yunge Zhu + +version_added: "2.7" + +short_description: Look up Azure service principal attributes. + +description: + - Describes object id of your Azure service principal account. +options: + azure_client_id: + description: azure service principal client id. + azure_secret: + description: azure service principal secret + azure_tenant: + description: azure tenant + azure_cloud_environment: + description: azure cloud environment +""" + +EXAMPLES = """ +set_fact: + object_id: "{{ lookup('azure_service_principal_attribute', + azure_client_id=azure_client_id, + azure_secret=azure_secret, + azure_tenant=azure_secret) }}" +""" + +RETURN = """ +_raw: + description: + Returns object id of service principal. +""" + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_native + +try: + from azure.common.credentials import ServicePrincipalCredentials + from azure.graphrbac import GraphRbacManagementClient + from azure.cli.core import cloud as azure_cloud +except ImportError: + raise AnsibleError( + "The lookup azure_service_principal_attribute requires azure.graphrbac, msrest") + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + + self.set_options(direct=kwargs) + + credentials = {} + credentials['azure_client_id'] = self.get_option('azure_client_id', None) + credentials['azure_secret'] = self.get_option('azure_secret', None) + credentials['azure_tenant'] = self.get_option('azure_tenant', 'common') + + if credentials['azure_client_id'] is None or credentials['azure_secret'] is None: + raise AnsibleError("Must specify azure_client_id and azure_secret") + + _cloud_environment = azure_cloud.AZURE_PUBLIC_CLOUD + if self.get_option('azure_cloud_environment', None) is not None: + cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(credentials['azure_cloud_environment']) + + try: + azure_credentials = ServicePrincipalCredentials(client_id=credentials['azure_client_id'], + secret=credentials['azure_secret'], + tenant=credentials['azure_tenant'], + resource=_cloud_environment.endpoints.active_directory_graph_resource_id) + + client = GraphRbacManagementClient(azure_credentials, credentials['azure_tenant'], + base_url=_cloud_environment.endpoints.active_directory_graph_resource_id) + + response = list(client.service_principals.list(filter="appId eq '{0}'".format(credentials['azure_client_id']))) + sp = response[0] + + return sp.object_id.split(',') + except Exception as ex: + raise AnsibleError("Failed to get service principal object id: %s" % to_native(ex)) + return False diff --git a/tests/integration/targets/azure_rm_virtualmachine/tasks/azure_test_encrypted.yml b/tests/integration/targets/azure_rm_virtualmachine/tasks/azure_test_encrypted.yml new file mode 100644 index 000000000..c43d09a61 --- /dev/null +++ b/tests/integration/targets/azure_rm_virtualmachine/tasks/azure_test_encrypted.yml @@ -0,0 +1,107 @@ +- name: Set variables + ansible.builtin.include_tasks: setup.yml + +- name: Set up disk encryption sets + ansible.builtin.include_tasks: setup_des.yml + +- name: Create VM with encrypted disks + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}" + admin_username: "testuser" + ssh_password_enabled: false + ssh_public_keys: + - path: /home/testuser/.ssh/authorized_keys + key_data: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDfoYlIV4lTPZTv7hXaVwQQuqBgGs4yeNRX0SPo2+HQt9u4X7IGwrtXc0nEUm6LfaCikMH58bOL8f20NTGz285kxdFHZRcBXtqmnMz2rXwhK9gwq5h1khc+GzHtdcJXsGA4y0xuaNcidcg04jxAlN/06fwb/VYwwWTVbypNC0gpGEpWckCNm8vlDlA55sU5et0SZ+J0RKVvEaweUOeNbFZqckGPA384imfeYlADppK/7eAxqfBVadVvZG8IJk4yvATgaIENIFj2cXxqu2mQ/Bp5Wr45uApvJsFXmi+v/nkiOEV1QpLOnEwAZo6EfFS4CCQtsymxJCl1PxdJ5LD4ZOtP xiuxi.sun@qq.com" + vm_size: Standard_B1ms + virtual_network: "{{ network_name }}" + os_disk_encryption_set: "{{ des_results.state.id }}" + managed_disk_type: Standard_LRS + image: + offer: 0001-com-ubuntu-server-focal + publisher: Canonical + sku: 20_04-lts + version: latest + register: vm_output + +- name: Query auto created security group before deleting + azure_rm_securitygroup_info: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}01" + register: nsg_result + +- name: Assert that security group were exist before deleting + ansible.builtin.assert: + that: + - nsg_result.securitygroups | length == 1 + - nsg_result.securitygroups[0].network_interfaces | length == 1 + +- name: Delete VM + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}" + remove_on_absent: all_autocreated + state: absent + +- name: Destroy encrypted OS disk + azure_rm_manageddisk: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}" + state: absent + +- name: Destroy auto created NIC + azure_rm_networkinterface: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}01" + state: absent + register: nic_result + +- name: Destroy security group + azure_rm_securitygroup: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}01" + state: absent + +- name: Destroy auto created public IP + azure_rm_publicipaddress: + resource_group: "{{ resource_group }}" + name: "{{ vm_name }}01" + state: absent + +- name: Destroy subnet + azure_rm_subnet: + resource_group: "{{ resource_group }}" + virtual_network: "{{ network_name }}" + name: "{{ subnet_name }}" + state: absent + +- name: Destroy virtual network + azure_rm_virtualnetwork: + resource_group: "{{ resource_group }}" + name: "{{ network_name }}" + state: absent + +- name: Destroy availability set + azure_rm_availabilityset: + resource_group: "{{ resource_group }}" + name: "{{ availability_set }}" + state: absent + +- name: Destroy storage account + azure_rm_storageaccount: + resource_group: "{{ resource_group }}" + name: "{{ storage_account }}" + force_delete_nonempty: true + state: absent + +- name: Destroy disk encryption set + azure_rm_diskencryptionset: + resource_group: "{{ resource_group }}" + name: "{{ des_name }}" + state: absent + +- name: Destroy key vault + azure_rm_keyvault: + vault_name: "{{ vault_name }}" + resource_group: "{{ resource_group }}" + state: absent diff --git a/tests/integration/targets/azure_rm_virtualmachine/tasks/setup_des.yml b/tests/integration/targets/azure_rm_virtualmachine/tasks/setup_des.yml new file mode 100644 index 000000000..1f62d37da --- /dev/null +++ b/tests/integration/targets/azure_rm_virtualmachine/tasks/setup_des.yml @@ -0,0 +1,94 @@ +- name: Set vault name + ansible.builtin.set_fact: + vault_name: "kv{{ uid_short }}{{ '%m%d%H%M%S' | strftime }}" + +- name: Lookup service principal object id + ansible.builtin.set_fact: + object_id: "{{ lookup('azure_service_principal_attribute', + azure_client_id=azure_client_id, + azure_secret=azure_secret, + azure_tenant=azure_tenant) }}" + register: object_id_facts + +- name: Create a key vault + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + vault_name: "{{ vault_name }}" + enabled_for_disk_encryption: true + enable_purge_protection: true + vault_tenant: "{{ azure_tenant }}" + sku: + name: standard + family: A + access_policies: + - tenant_id: "{{ azure_tenant }}" + object_id: "{{ object_id }}" + keys: + - get + - list + - wrapkey + - unwrapkey + - create + - update + - import + - delete + - backup + - restore + - recover + - purge + +- name: Create a key in key vault + azure_rm_keyvaultkey: + key_name: testkey + keyvault_uri: https://{{ vault_name }}.vault.azure.net + +- name: Get latest version of key + azure_rm_keyvaultkey_info: + vault_uri: https://{{ vault_name }}.vault.azure.net + name: testkey + register: results + +- name: Assert the key vault facts + ansible.builtin.set_fact: + key_url: "{{ results['keys'][0]['kid'] }}" + +- name: Create disk encryption set + azure_rm_diskencryptionset: + resource_group: "{{ resource_group }}" + name: "{{ des_name }}" + source_vault: "{{ vault_name }}" + key_url: "{{ key_url }}" + state: present + register: des_results + +- name: Grant DES access to key vault + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + vault_name: "{{ vault_name }}" + enabled_for_disk_encryption: true + enable_purge_protection: true + vault_tenant: "{{ azure_tenant }}" + sku: + name: standard + family: A + access_policies: + - tenant_id: "{{ azure_tenant }}" + object_id: "{{ object_id }}" + keys: + - get + - list + - wrapkey + - unwrapkey + - create + - update + - import + - delete + - backup + - restore + - recover + - purge + - object_id: "{{ des_results.state.identity.principal_id }}" + keys: + - get + - wrapkey + - unwrapkey