Skip to content

Commit

Permalink
Add os_disk_encryption_set to azure_rm_virtualmachine (#1306)
Browse files Browse the repository at this point in the history
* Add os_disk_encryption_set to azure_rm_virtualmachine

Add the parameter `os_disk_encryption_set` to the
`azure_rm_virtualmachine` module, making it possible to specify which
DES to use when encrypting the OS disk for a newly created VM.

* Improved checking update of DES for OSDisk

To ensure that we won't try to *add* or *change* DES on an existing VM,
the logic to check existing DES setting and comparing it to the desired
state is fixed to handle both cases.

The code is also polished to make it more readable.

* Add integration tests

Add a new kind of virtual machine tests
which creates VMs with encrypted OS disk
using the new property `os_disk_encryption_set`.

I also added the generated inventory to
`.gitignore` to avoid comitting it by mistake.

* Polish the code

* Fix os disk encryption in tests

Ensure that the OS disk is encrypted in the integration tests.

This requires the DES to be granted access to the KeyVault, and
that the KeyVault is protected against purges. To avoid that we
get conflicts with previously deleted, but unpurged KVs, they are
named with a date-based suffix.
  • Loading branch information
ephracis authored Nov 14, 2023
1 parent ee30130 commit 160466c
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ venv*
.vscode
ansible_collections/
.idea/
tests/integration/inventory
20 changes: 19 additions & 1 deletion plugins/modules/azure_rm_virtualmachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'] = {}
Expand Down Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# (c) 2018 Yunge Zhu, <[email protected]>
# (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 <[email protected]>
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
Original file line number Diff line number Diff line change
@@ -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 [email protected]"
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
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 160466c

Please sign in to comment.