From 3c01b956b239e346ec849520faeb893b0a169d93 Mon Sep 17 00:00:00 2001 From: theko2fi <72862222+theko2fi@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:24:38 +0100 Subject: [PATCH 1/8] added mount feature * add `multipass_mount` module * add `mounts` option to `multipass_vm` module --- .gitignore | 4 +- README.md | 1 + plugins/module_utils/errors.py | 10 +- plugins/module_utils/multipass.py | 64 ++- plugins/modules/multipass_mount.py | 185 +++++++ plugins/modules/multipass_vm.py | 478 ++++++++++++------ .../targets/multipass_mount/tasks/main.yml | 195 +++++++ .../targets/multipass_vm/tasks/main.yml | 10 +- .../targets/multipass_vm/tasks/mount.yml | 126 +++++ 9 files changed, 901 insertions(+), 172 deletions(-) create mode 100644 plugins/modules/multipass_mount.py create mode 100644 tests/integration/targets/multipass_mount/tasks/main.yml create mode 100644 tests/integration/targets/multipass_vm/tasks/mount.yml diff --git a/.gitignore b/.gitignore index c87ed7c..b63de4d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ github_action_key* .vscode/ dfdf.json test-playbook.yml -changelogs/.plugin-cache.yaml \ No newline at end of file +changelogs/.plugin-cache.yaml +plugins/modules/__pycache__/ +ansible.cfg \ No newline at end of file diff --git a/README.md b/README.md index ba3e015..01bacc1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Please note that this collection is **not** developed by [Canonical](https://can - [multipass_vm_purge](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_vm_purge_module.html) - Module to purge all deleted Multipass virtual machines permanently - [multipass_vm_transfer_into](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_vm_transfer_into_module.html) - Module to copy a file into a Multipass virtual machine - [multipass_config_get](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_config_get_module.html) - Module to get Multipass configuration setting + - [multipass_mount](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_mount_module.html) - Module to manage directory mapping between host and Multipass virtual machines ## Installation diff --git a/plugins/module_utils/errors.py b/plugins/module_utils/errors.py index 6a7edf6..3afdee0 100644 --- a/plugins/module_utils/errors.py +++ b/plugins/module_utils/errors.py @@ -1,9 +1,15 @@ #!/usr/bin/python # -# Copyright (c) 2022, Kenneth KOFFI +# Copyright (c) 2024, Kenneth KOFFI (https://www.linkedin.com/in/kenneth-koffi-6b1218178/) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later +class MountExistsError(Exception): + pass + +class MountNonExistentError(Exception): + pass + class MultipassFileTransferError(Exception): pass @@ -11,4 +17,4 @@ class MultipassContentTransferError(Exception): pass class SocketError(Exception): - pass \ No newline at end of file + pass diff --git a/plugins/module_utils/multipass.py b/plugins/module_utils/multipass.py index 961202e..3c6adf4 100644 --- a/plugins/module_utils/multipass.py +++ b/plugins/module_utils/multipass.py @@ -5,8 +5,11 @@ import json import time from shlex import split as shlexsplit -from .errors import SocketError +from .errors import SocketError, MountNonExistentError, MountExistsError +def get_existing_mounts(vm_name): + vm = MultipassClient().get_vm(vm_name) + return vm.info().get('info').get(vm_name).get("mounts") # Added decorator to automatically retry on unpredictable module failures def retry_on_failure(ExceptionsToCheck, max_retries=5, delay=5, backoff=2): @@ -29,6 +32,7 @@ class MultipassVM: def __init__(self, vm_name, multipass_cmd): self.cmd = multipass_cmd self.vm_name = vm_name + # Will retry to execute info() if SocketError occurs @retry_on_failure(ExceptionsToCheck=SocketError) def info(self): @@ -48,6 +52,7 @@ def info(self): else: raise Exception("Multipass info command failed: {0}".format(stderr.decode(encoding="utf-8"))) return json.loads(stdout) + def delete(self, purge=False): cmd = [self.cmd, "delete", self.vm_name] if purge: @@ -67,8 +72,10 @@ def delete(self, purge=False): self.vm_name, stderr.decode(encoding="utf-8") ) ) + def shell(self): raise Exception("The shell command is not supported in the Multipass SDK. Consider using exec.") + def exec(self, cmd_to_execute, working_directory=""): cmd = [self.cmd, "exec", self.vm_name] if working_directory: @@ -84,18 +91,21 @@ def exec(self, cmd_to_execute, working_directory=""): if(exitcode != 0): raise Exception("Multipass exec command failed: {0}".format(stderr.decode(encoding="utf-8"))) return stdout, stderr + def stop(self): cmd = [self.cmd, "stop", self.vm_name] try: subprocess.check_output(cmd) except: raise Exception("Error stopping Multipass VM {0}".format(self.vm_name)) + def start(self): cmd = [self.cmd, "start", self.vm_name] try: subprocess.check_output(cmd) except: raise Exception("Error starting Multipass VM {0}".format(self.vm_name)) + def restart(self): cmd = [self.cmd, "restart", self.vm_name] try: @@ -109,6 +119,7 @@ class MultipassClient: """ def __init__(self, multipass_cmd="multipass"): self.cmd = multipass_cmd + def launch(self, vm_name=None, cpu=1, disk="5G", mem="1G", image=None, cloud_init=None): if(not vm_name): # similar to Multipass's VM name generator @@ -124,20 +135,24 @@ def launch(self, vm_name=None, cpu=1, disk="5G", mem="1G", image=None, cloud_ini except: raise Exception("Error launching Multipass VM {0}".format(vm_name)) return MultipassVM(vm_name, self.cmd) + def transfer(self, src, dest): cmd = [self.cmd, "transfer", src, dest] try: subprocess.check_output(cmd) except: raise Exception("Multipass transfer command failed.") + def get_vm(self, vm_name): return MultipassVM(vm_name, self.cmd) + def purge(self): cmd = [self.cmd, "purge"] try: subprocess.check_output(cmd) except: raise Exception("Purge command failed.") + def list(self): cmd = [self.cmd, "list", "--format", "json"] out = subprocess.Popen(cmd, @@ -148,6 +163,7 @@ def list(self): if(not exitcode == 0): raise Exception("Multipass list command failed: {0}".format(stderr)) return json.loads(stdout) + def find(self): cmd = [self.cmd, "find", "--format", "json"] out = subprocess.Popen(cmd, @@ -158,30 +174,52 @@ def find(self): if(not exitcode == 0): raise Exception("Multipass find command failed: {0}".format(stderr)) return json.loads(stdout) - def mount(self, src, target): - cmd = [self.cmd, "mount", src, target] - try: - subprocess.check_output(cmd) - except: - raise Exception("Multipass mount command failed.") - def unmount(self, mount): - cmd = [self.cmd, "unmount", mount] - try: - subprocess.check_output(cmd) - except: - raise Exception("Multipass unmount command failed.") + + def mount(self, src, target, mount_type='classic', uid_maps=[], gid_maps=[]): + mount_options = ["--type", mount_type] + for uid_map in uid_maps: + mount_options.extend(["--uid-map", uid_map]) + for gid_map in gid_maps: + mount_options.extend(["--gid-map", gid_map]) + cmd = [self.cmd, "mount"] + mount_options + [src, target] + out = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _,stderr = out.communicate() + exitcode = out.wait() + stderr_cleaned = stderr.decode(encoding="utf-8").splitlines() + if(not exitcode == 0): + for error_msg in stderr_cleaned: + if 'is already mounted' in error_msg: + raise MountExistsError + raise Exception("Multipass mount command failed: {0}".format(stderr.decode(encoding="utf-8").rstrip())) + + def umount(self, mount): + cmd = [self.cmd, "umount", mount] + out = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + _,stderr = out.communicate() + exitcode = out.wait() + stderr_cleaned = stderr.decode(encoding="utf-8").splitlines() + if(not exitcode == 0): + for error_msg in stderr_cleaned: + if 'is not mounted' in error_msg: + raise MountNonExistentError + raise Exception("{}".format(stderr.decode(encoding="utf-8").rstrip())) + def recover(self, vm_name): cmd = [self.cmd, "recover", vm_name] try: subprocess.check_output(cmd) except: raise Exception("Multipass recover command failed.") + def suspend(self): cmd = [self.cmd, "suspend"] try: subprocess.check_output(cmd) except: raise Exception("Multipass suspend command failed.") + def get(self, key): cmd = [self.cmd, "get", key] out = subprocess.Popen(cmd, diff --git a/plugins/modules/multipass_mount.py b/plugins/modules/multipass_mount.py new file mode 100644 index 0000000..4b19b07 --- /dev/null +++ b/plugins/modules/multipass_mount.py @@ -0,0 +1,185 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# +# Copyright 2023 Kenneth KOFFI (@theko2fi) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from ansible.module_utils.basic import AnsibleModule +from ansible.errors import AnsibleError +from ansible_collections.theko2fi.multipass.plugins.module_utils.multipass import MultipassClient, get_existing_mounts +from ansible_collections.theko2fi.multipass.plugins.module_utils.errors import MountExistsError, MountNonExistentError + + + +multipassclient = MultipassClient() + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name = dict(required=True, type='str'), + source = dict(type='str'), + target = dict(required=False, type='str'), + state = dict(required=False, type=str, default='present', choices=['present','absent']), + type = dict(type='str', required=False, default='classic', choices=['classic','native']), + gid_map = dict(type='list', elements='str', required=False, default=[]), + uid_map = dict(type='list', elements='str', required=False, default=[]) + ), + required_if = [('state', 'present', ['source'])] + ) + + vm_name = module.params.get('name') + src = module.params.get('source') + dest = module.params.get('target') + state = module.params.get('state') + gid_map = module.params.get('gid_map') + uid_map = module.params.get('uid_map') + mount_type = module.params.get('type') + + if state in ('present'): + dest = dest or src + target = f"{vm_name}:{dest}" + try: + multipassclient.mount(src=src, target=target, uid_maps=uid_map, gid_maps=gid_map, mount_type=mount_type ) + module.exit_json(changed=True, result=get_existing_mounts(vm_name).get(dest)) + except MountExistsError: + module.exit_json(changed=False, result=get_existing_mounts(vm_name).get(dest)) + except Exception as e: + module.fail_json(msg=str(e)) + else: + target = f"{vm_name}:{dest}" if dest else vm_name + try: + changed = False if not get_existing_mounts(vm_name=vm_name) else True + multipassclient.umount(mount=target) + module.exit_json(changed=changed) + except MountNonExistentError: + module.exit_json(changed=False) + except Exception as e: + module.fail_json(msg=str(e)) + + + +if __name__ == "__main__": + main() + + + +DOCUMENTATION = ''' +module: multipass_mount +short_description: Module to manage directory mapping between host and Multipass virtual machine +description: + - Mount a local directory in a Multipass virtual machine. + - Unmount a directory from a Multipass virtual machine. +version_added: 0.3.0 +options: + name: + type: str + description: + - Name of the virtual machine to operate on. + required: true + source: + type: str + description: + - Path of the local directory to mount + - Use with O(state=present) to mount the local directory inside the VM. + required: false + target: + type: str + description: + - target mount point (path inside the VM). + - If omitted when O(state=present), the mount point will be the same as the source's absolute path. + - If omitted when O(state=absent), all mounts will be removed from the named VM. + required: false + type: + description: + - Specify the type of mount to use. + - V(classic) mounts use technology built into Multipass. + - V(native) mounts use hypervisor and/or platform specific mounts. + type: str + default: classic + choices: + - classic + - native + gid_map: + description: + - A list of group IDs mapping for use in the mount. + - Use the Multipass CLI syntax C(:). + - File and folder ownership will be mapped from to inside the VM. + type: list + elements: str + default: [] + uid_map: + description: + - A list of user IDs mapping for use in the mount. + - Use the Multipass CLI syntax C(:). + - File and folder ownership will be mapped from to inside the VM. + type: list + elements: str + default: [] + state: + description: + - V(absent) Unmount the O(target) mount point from the VM. + - V(present) Mount the O(source) directory inside the VM. If the VM is not currently running, the directory will be mounted automatically on next boot. + type: str + default: present + choices: + - absent + - present +author: + - Kenneth KOFFI (@theko2fi) +''' + +EXAMPLES = ''' +- name: Mount '/root/data' directory from the host to '/root/data' inside the VM named 'healthy-cankerworm' + theko2fi.multipass.multipass_mount: + name: healthy-cankerworm + source: /root/data + +- name: Mount '/root/data' to '/tmp' inside the VM + theko2fi.multipass.multipass_mount: + name: healthy-cankerworm + source: /root/data + target: /tmp + state: present + +- name: Unmount '/tmp' directory from the VM + theko2fi.multipass.multipass_mount: + name: healthy-cankerworm + target: /tmp + state: absent + +- name: Mount directory, set file and folder ownership + theko2fi.multipass.multipass_mount: + name: healthy-cankerworm + source: /root/data + target: /tmp + state: present + type: classic + uid_map: + - "50:50" + - "1000:1000" + gid_map: + - "50:50" + +- name: Unmount all mount points from the 'healthy-cankerworm' VM + theko2fi.multipass.multipass_mount: + name: healthy-cankerworm + state: absent +''' + +RETURN = ''' +result: + description: + - Empty if O(state=absent). + returned: when O(state=present) + type: dict + sample: { + "gid_mappings": [ + "0:default" + ], + "source_path": "/root/tmp", + "uid_mappings": [ + "0:default" + ] + } +''' \ No newline at end of file diff --git a/plugins/modules/multipass_vm.py b/plugins/modules/multipass_vm.py index 3ac2aef..f1e9e29 100644 --- a/plugins/modules/multipass_vm.py +++ b/plugins/modules/multipass_vm.py @@ -1,127 +1,240 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # -# Copyright 2023 Kenneth KOFFI <@theko2fi> +# Copyright 2023 Kenneth KOFFI (@theko2fi) # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from ansible.module_utils.basic import AnsibleModule -from ansible_collections.theko2fi.multipass.plugins.module_utils.multipass import MultipassClient +from ansible_collections.theko2fi.multipass.plugins.module_utils.multipass import MultipassClient, get_existing_mounts +import os, sys multipassclient = MultipassClient() -def is_vm_exists(vm_name): - vm_local = multipassclient.get_vm(vm_name=vm_name) - try: - vm_local.info() - return True - except NameError: - return False - -def get_vm_state(vm_name: str): - if is_vm_exists(vm_name=vm_name): - vm = multipassclient.get_vm(vm_name=vm_name) - vm_info = vm.info() - vm_state = vm_info.get("info").get(vm_name).get("state") - return vm_state - -def is_vm_deleted(vm_name: str): - vm_state = get_vm_state(vm_name=vm_name) - return vm_state == 'Deleted' - -def is_vm_running(vm_name: str): - vm_state = get_vm_state(vm_name=vm_name) - return vm_state == 'Running' - -def is_vm_stopped(vm_name: str): - vm_state = get_vm_state(vm_name=vm_name) - return vm_state == 'Stopped' +class AnsibleMultipassVM: + def __init__(self, name): + self.name = name + self.vm = multipassclient.get_vm(vm_name=name) -def main(): - module = AnsibleModule( - argument_spec=dict( - name = dict(required=True, type='str'), - image = dict(required=False, type=str, default='ubuntu-lts'), - cpus = dict(required=False, type=int, default=1), - memory = dict(required=False, type=str, default='1G'), - disk = dict(required=False, type=str, default='5G'), - cloud_init = dict(required=False, type=str, default=None), - state = dict(required=False, type=str, default='present'), - recreate = dict(required=False, type=bool, default=False), - purge = dict(required=False, type=bool, default=False) - ) - ) + @property + def is_vm_exists(self): + try: + self.vm.info() + return True + except NameError: + return False + + def get_vm_state(self): + vm_state = self.vm.info().get("info").get(self.name).get("state") + return vm_state + + @property + def is_vm_deleted(self) -> bool: + return self.get_vm_state() == 'Deleted' + + @property + def is_vm_running(self) -> bool: + return self.get_vm_state() == 'Running' + + @property + def is_vm_stopped(self) -> bool: + vm_state = self.get_vm_state() + return vm_state == 'Stopped' + +def compare_dictionaries(dict1, dict2): + # Check for keys present in dict1 but not in dict2 + keys_only_in_dict1 = set(dict1.keys()) - set(dict2.keys()) - vm_name = module.params.get('name') - image = module.params.get('image') - cpus = module.params.get('cpus') - state = module.params.get('state') - memory = module.params.get('memory') - disk = module.params.get('disk') - cloud_init = module.params.get('cloud_init') - purge = module.params.get('purge') - - if state in ('present', 'started'): - try: - if not is_vm_exists(vm_name): - vm = multipassclient.launch(vm_name=vm_name, image=image, cpu=cpus, mem=memory, disk=disk, cloud_init=cloud_init) - module.exit_json(changed=True, result=vm.info()) - else: - vm = multipassclient.get_vm(vm_name=vm_name) - if module.params.get('recreate'): - vm.delete(purge=True) - vm = multipassclient.launch(vm_name=vm_name, image=image, cpu=cpus, mem=memory, disk=disk, cloud_init=cloud_init) - module.exit_json(changed=True, result=vm.info()) - - if state == 'started': - # we do nothing if the VM is already running - if is_vm_running(vm_name): - module.exit_json(changed=False, result=vm.info()) - else: - # we recover the VM if it was deleted - if is_vm_deleted(vm_name): - multipassclient.recover(vm_name=vm_name) - # We start the VM if it isn't running - vm.start() - module.exit_json(changed=True, result=vm.info()) - - # we do nothing if the VM is already present - module.exit_json(changed=False, result=vm.info()) - except Exception as e: - module.fail_json(msg=str(e)) - - if state in ('absent', 'stopped'): - try: - if not is_vm_exists(vm_name=vm_name): - module.exit_json(changed=False) - else: - vm = multipassclient.get_vm(vm_name=vm_name) - - if state == 'stopped': - # we do nothing if the VM is already stopped - if is_vm_stopped(vm_name=vm_name): - module.exit_json(changed=False) - elif is_vm_deleted(vm_name=vm_name): - module.exit_json(changed=False) - else: - # stop the VM if it's running - vm.stop() - module.exit_json(changed=True) - - try: - vm.delete(purge=purge) - module.exit_json(changed=True) - except NameError: - # we do nothing if the VM doesn't exist - module.exit_json(changed=False) - except Exception as e: - module.fail_json(msg=str(e)) - + # Check for keys present in dict2 but not in dict1 + keys_only_in_dict2 = set(dict2.keys()) - set(dict1.keys()) + + # Check for common keys and compare values + keys_with_different_values = {key for key in set(dict1.keys()) & set(dict2.keys()) if dict1[key] != dict2[key]} + + if not keys_only_in_dict1 and not keys_only_in_dict2 and not keys_with_different_values: + is_different = False + else: + is_different = True + + return is_different, keys_only_in_dict1, keys_only_in_dict2, keys_with_different_values + +def build_expected_mounts_dictionnary(mounts: list): + + expected_mounts = dict() + + for mount in mounts: + source = mount.get('source') + target = mount.get('target') or source + # By default, Multipass use the current process gid and uid for mappings + # On Windows, there is no direct equivalent to the Unix-like concept of user ID (UID) + # So Multipass seems to use "-2" as default values on Windows (need to be verified) + gid_mappings = mount.get('gid_map') or [f"{os.getgid()}:default"] if sys.platform not in ("win32","win64") else ["-2:default"] + uid_mappings = mount.get('uid_map') or [f"{os.getuid()}:default"] if sys.platform not in ("win32","win64") else ["-2:default"] + expected_mounts[target] = { + "gid_mappings": gid_mappings, + "source_path": source, + "uid_mappings": uid_mappings + } + return expected_mounts + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name = dict(required=True, type='str'), + image = dict(required=False, type=str, default='ubuntu-lts'), + cpus = dict(required=False, type=int, default=1), + memory = dict(required=False, type=str, default='1G'), + disk = dict(required=False, type=str, default='5G'), + cloud_init = dict(required=False, type=str, default=None), + state = dict(required=False, type=str, default='present'), + recreate = dict(required=False, type=bool, default=False), + purge = dict(required=False, type=bool, default=False), + mounts = dict(type='list', elements='dict', suboptions=dict( + target=dict(type='str'), + source=dict(type='str', required=True), + type=dict(type='str', choices=['classic', 'native'], default='classic'), + gid_map=dict(type='list', elements='str'), + uid_map=dict(type='list', elements='str') + ) + ) + ) + ) + + vm_name = module.params.get('name') + image = module.params.get('image') + cpus = module.params.get('cpus') + state = module.params.get('state') + memory = module.params.get('memory') + disk = module.params.get('disk') + cloud_init = module.params.get('cloud_init') + purge = module.params.get('purge') + mounts = module.params.get('mounts') + + ansible_multipass = AnsibleMultipassVM(vm_name) + + if state in ('present', 'started'): + try: + # we create a new VM + if not ansible_multipass.is_vm_exists: + vm = multipassclient.launch( + vm_name=vm_name, + image=image, + cpu=cpus, + mem=memory, + disk=disk, + cloud_init=cloud_init + ) + changed = True + #module.exit_json(changed=True, result=vm.info()) + else: + vm = ansible_multipass.vm + changed = False + + if state == 'started': + # # we do nothing if the VM is already running + # if is_vm_running(vm_name): + # changed=False + # #module.exit_json(changed=False, result=vm.info()) + if not ansible_multipass.is_vm_running: + # we recover the VM if it was deleted + if ansible_multipass.is_vm_deleted: + multipassclient.recover(vm_name=vm_name) + # We start the VM if it isn't running + vm.start() + changed = True + #module.exit_json(changed=True, result=vm.info()) + + if module.params.get('recreate'): + vm.delete(purge=True) + vm = multipassclient.launch( + vm_name=vm_name, + image=image, + cpu=cpus, + mem=memory, + disk=disk, + cloud_init=cloud_init + ) + changed = True + #module.exit_json(changed=True, result=vm.info()) + + if mounts: + existing_mounts = get_existing_mounts(vm_name=vm_name) + expected_mounts = build_expected_mounts_dictionnary(mounts) + # Compare existing and expected mounts + is_different, target_paths_only_in_expected_mounts, target_paths_only_in_existing_mounts, different_mounts = compare_dictionaries(expected_mounts, existing_mounts) + if is_different: + changed = True + for target_path in target_paths_only_in_existing_mounts: + MultipassClient().umount(mount=f"{vm_name}:{target_path}") + + for target_path in target_paths_only_in_expected_mounts: + source_path = expected_mounts.get(target_path).get("source_path") + uid_mappings = expected_mounts.get(target_path).get("uid_mappings") + gid_mappings = expected_mounts.get(target_path).get("gid_mappings") + + uid_mappings_cleaned = [uid_mapping for uid_mapping in uid_mappings if not "default" in uid_mapping] + gid_mappings_cleaned = [gid_mapping for gid_mapping in gid_mappings if not "default" in gid_mapping] + + MultipassClient().mount( + src=source_path, + target=f"{vm_name}:{target_path}", + uid_maps=uid_mappings_cleaned, + gid_maps=gid_mappings_cleaned + ) + + for target_path in different_mounts: + MultipassClient().umount(mount=f"{vm_name}:{target_path}") + + source_path = expected_mounts.get(target_path).get("source_path") + uid_mappings = expected_mounts.get(target_path).get("uid_mappings") + gid_mappings = expected_mounts.get(target_path).get("gid_mappings") + + uid_mappings_cleaned = [uid_mapping for uid_mapping in uid_mappings if not "default" in uid_mapping] + gid_mappings_cleaned = [gid_mapping for gid_mapping in gid_mappings if not "default" in gid_mapping] + + MultipassClient().mount( + src=source_path, + target=f"{vm_name}:{target_path}", + uid_maps=uid_mappings_cleaned, + gid_maps=gid_mappings_cleaned + ) + + + module.exit_json(changed=changed, result=vm.info()) + except Exception as e: + module.fail_json(msg=str(e)) + + if state in ('absent', 'stopped'): + try: + if not ansible_multipass.is_vm_exists: + module.exit_json(changed=False) + else: + if state == 'stopped': + # we do nothing if the VM is already stopped + if ansible_multipass.is_vm_stopped: + module.exit_json(changed=False) + elif ansible_multipass.is_vm_deleted: + module.exit_json(changed=False) + else: + # stop the VM if it's running + ansible_multipass.vm.stop() + module.exit_json(changed=True) + + try: + ansible_multipass.vm.delete(purge=purge) + module.exit_json(changed=True) + except NameError: + # we do nothing if the VM doesn't exist + module.exit_json(changed=False) + except Exception as e: + module.fail_json(msg=str(e)) + if __name__ == "__main__": - main() + main() DOCUMENTATION = ''' @@ -131,14 +244,14 @@ def main(): short_description: Module to manage Multipass VM description: - Manage the life cycle of Multipass virtual machines (create, start, stop, - delete). + delete). options: name: description: - Name for the VM. - If it is C('primary') (the configured primary instance name), the user's - home directory is mounted inside the newly launched instance, in - C('Home'). + home directory is mounted inside the newly launched instance, in + C('Home'). required: yes type: str image: @@ -155,11 +268,57 @@ def main(): required: false type: str default: 1G + mounts: + type: list + elements: dict + required: false + description: + - Specification for mounts to be added to the VM. + - Omitting a mount that is currently applied to a VM will remove it. + version_added: 0.3.0 + suboptions: + source: + type: str + description: + - Path of the local directory to mount. + required: true + target: + type: str + description: + - target mount point (path inside the VM). + - If omitted, the mount point will be the same as the source's absolute path. + required: false + type: + type: str + description: + - Specify the type of mount to use. + - V(classic) mounts use technology built into Multipass. + - V(native) mounts use hypervisor and/or platform specific mounts. + default: classic + choices: + - classic + - native + uid_map: + description: + - A list of user IDs mapping for use in the mount. + - Use the Multipass CLI syntax C(:). + File and folder ownership will be mapped from to inside the VM. + - Omitting an uid_map that is currently applied to a mount, will remove it. + type: list + elements: str + gid_map: + description: + - A list of group IDs mapping for use in the mount. + - Use the Multipass CLI syntax C(:). + File and folder ownership will be mapped from to inside the VM. + - Omitting an gid_map that is currently applied to a mount, will remove it. + type: list + elements: str disk: description: - Disk space to allocate to the VM in format C([]). - Positive integers, in bytes, or with V(K) (kibibyte, 1024B), V(M) - (mebibyte), V(G) (gibibyte) suffix. + (mebibyte), V(G) (gibibyte) suffix. - Omitting the unit defaults to bytes. required: false type: str @@ -172,17 +331,17 @@ def main(): state: description: - C(absent) - An instance matching the specified name will be stopped and - deleted. + deleted. - C(present) - Asserts the existence of an instance matching the name and - any provided configuration parameters. If no instance matches the name, - a virtual machine will be created. + any provided configuration parameters. If no instance matches the name, + a virtual machine will be created. - 'V(started) - Asserts that the VM is first V(present), and then if the VM - is not running moves it to a running state. If the VM was deleted, it will - be recovered and started.' + is not running moves it to a running state. If the VM was deleted, it will + be recovered and started.' - 'V(stopped) - If an instance matching the specified name is running, moves - it to a stopped state.' + it to a stopped state.' - Use the O(recreate) option to always force re-creation of a matching virtual - machine, even if it is running. + machine, even if it is running. required: false type: str default: present @@ -197,7 +356,7 @@ def main(): default: false recreate: description: Use with O(state=present) or O(state=started) to force the re-creation - of an existing virtual machine. + of an existing virtual machine. type: bool default: false ''' @@ -236,7 +395,7 @@ def main(): theko2fi.multipass.multipass_vm: name: foo state: absent - + - name: Delete and purge a VM theko2fi.multipass.multipass_vm: name: foo @@ -247,47 +406,56 @@ def main(): RETURN = ''' --- result: - description: return the VM info + description: return the VM info ''' RETURN = ''' result: - description: + description: - Facts representing the current state of the virtual machine. Matches the multipass info output. - Empty if O(state=absent) or O(state=stopped). - Will be V(none) if virtual machine does not exist. - returned: success; or when O(state=started) or O(state=present), and when waiting for the VM result did not fail - type: dict - sample: '{ - "errors": [], - "info": { - "foo": { - "cpu_count": "1", - "disks": { - "sda1": { - "total": "5120710656", - "used": "2200540672" - } - }, - "image_hash": "fe102bfb3d3d917d31068dd9a4bd8fcaeb1f529edda86783f8524fdc1477ee29", - "image_release": "22.04 LTS", - "ipv4": [ - "172.23.240.92" - ], - "load": [ - 0.01, - 0.01, - 0 - ], - "memory": { - "total": 935444480, - "used": 199258112 - }, - "mounts": { - }, - "release": "Ubuntu 22.04.2 LTS", - "state": "Running" + returned: success; or when O(state=started) or O(state=present), and when waiting for the VM result did not fail + type: dict + sample: '{ + "errors": [], + "info": { + "foo": { + "cpu_count": "1", + "disks": { + "sda1": { + "total": "5120710656", + "used": "2200540672" + } + }, + "image_hash": "fe102bfb3d3d917d31068dd9a4bd8fcaeb1f529edda86783f8524fdc1477ee29", + "image_release": "22.04 LTS", + "ipv4": [ + "172.23.240.92" + ], + "load": [ + 0.01, + 0.01, + 0 + ], + "memory": { + "total": 935444480, + "used": 199258112 + }, + "mounts": { + "/home/ubuntu/data": { + "gid_mappings": [ + "0:default" + ], + "source_path": "/tmp", + "uid_mappings": [ + "0:default" + ] + } + }, + "release": "Ubuntu 22.04.2 LTS", + "state": "Running" + } } - } }' ''' \ No newline at end of file diff --git a/tests/integration/targets/multipass_mount/tasks/main.yml b/tests/integration/targets/multipass_mount/tasks/main.yml new file mode 100644 index 0000000..bcc51e5 --- /dev/null +++ b/tests/integration/targets/multipass_mount/tasks/main.yml @@ -0,0 +1,195 @@ +--- + +- name: Set facts + set_fact: + vm_name: "{{ 'ansible-multipass-mount-test-%0x' % ((2**32) | random) }}" + mount_source_for_test: "{{ansible_env.HOME}}/foo_bar" + +- name: Delete any existing VM + theko2fi.multipass.multipass_vm: + name: "{{vm_name}}" + state: absent + purge: true + +- name: Ensure that the VM exist for the test + theko2fi.multipass.multipass_vm: + name: "{{vm_name}}" + state: present + +- name: Create '{{ mount_source_for_test }}' directory if it does not exist + ansible.builtin.file: + path: "{{ mount_source_for_test }}" + state: directory + +- name: Mount '{{ mount_source_for_test }}' directory from the host to '{{ mount_source_for_test }}' inside the VM named '{{vm_name}}' + theko2fi.multipass.multipass_mount: + name: "{{vm_name}}" + source: "{{ mount_source_for_test }}" + register: mount_output + +- name: Assert that {{ mount_source_for_test }} has been mounted to the VM + ansible.builtin.assert: + that: + - mount_output.changed + - mount_output.result is defined + - mount_output.result.source_path == mount_source_for_test + +- name: Mount '{{ mount_source_for_test }}' to '{{ mount_source_for_test }}' (check idempotency) + theko2fi.multipass.multipass_mount: + name: "{{vm_name}}" + source: "{{ mount_source_for_test }}" + register: mount_idempotency_output + +- name: Check idempotency + ansible.builtin.assert: + that: + - mount_idempotency_output.changed == False + +- name: Mount '{{ mount_source_for_test }}' to '/data' inside the VM, set ownership + theko2fi.multipass.multipass_mount: + name: "{{vm_name}}" + source: "{{ mount_source_for_test }}" + target: /data + state: present + uid_map: + - "50:50" + - "1000:1000" + gid_map: + - "50:50" + register: mount_to_data + +- name: Assert that {{ mount_source_for_test }} has been mounted to /data + ansible.builtin.assert: + that: + - mount_to_data.changed + - mount_to_data.result is defined + - mount_to_data.result.source_path == mount_source_for_test + - mount_to_data.result.uid_mappings == ["50:50", "1000:1000"] + - mount_to_data.result.gid_mappings == ["50:50"] + +- name: Unmount '{{ mount_source_for_test }}' directory from the VM + theko2fi.multipass.multipass_mount: + name: "{{vm_name}}" + target: "{{ mount_source_for_test }}" + state: absent + register: unmount + +- name: Get infos on virtual machine + theko2fi.multipass.multipass_vm_info: + name: "{{vm_name}}" + register: unmount_info + +- name: Assert that {{ mount_source_for_test }} has been unmounted + ansible.builtin.assert: + that: + - unmount.changed + - unmount_info.result.info[vm_name].mounts[mount_source_for_test] is not defined + - unmount_info.result.info[vm_name].mounts['/data'] is defined + +- name: Unmount all mount points from the '{{vm_name}}' VM + theko2fi.multipass.multipass_mount: + name: "{{vm_name}}" + state: absent + register: unmount_all + ignore_errors: true + +# TO DO: This need to be uncommented later After multipass the underlying bug: +# 'Failed to terminate SSHFS mount process' + +# - name: Get infos on virtual machine +# theko2fi.multipass.multipass_vm_info: +# name: "{{vm_name}}" +# register: unmount_all_info + +# - name: Assert that all mount points have been removed from the VM +# ansible.builtin.assert: +# that: +# - unmount_all.changed +# - unmount_all_info.result.info[vm_name].mounts is not defined + +# - name: Unmount all directories (idempotency check) +# theko2fi.multipass.multipass_mount: +# name: "{{vm_name}}" +# state: absent +# register: unmount_all_idempotency + +# - name: Assert Unmount all directories (idempotency check) +# ansible.builtin.assert: +# that: +# - unmount_all_idempotency.changed == False + +- name: Delete the VM + theko2fi.multipass.multipass_vm: + name: "{{vm_name}}" + state: absent + purge: true + +###### For windows devices ############# + +# - name: Mount a nonexistent source directory +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# source: "C:/Users/kenneth/Downloads/165.232.139.40xx" +# target: "/tmp" +# state: present +# register: eettt +# ignore_errors: true + +# - name: Debug +# ansible.builtin.debug: +# var: eettt + +# - name: Mount to a nonexistent instance +# theko2fi.multipass.multipass_mount: +# name: "heaaalthy-cankerworm" +# source: "C:/Users/kenneth/Downloads/165.232.139.40" +# target: "/tmp" +# state: present +# register: ee +# ignore_errors: true + +# - name: Debug +# ansible.builtin.debug: +# var: ee + +# - name: Mount a directory to an instance +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# source: "C:/Users/kenneth/Downloads/165.232.139.40" +# target: "/tmp" +# state: present +# register: mounted + +# - name: Debug +# ansible.builtin.debug: +# var: mounted + +# - name: Mount a directory to an instance (idempotency check) +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# source: "C:/Users/kenneth/Downloads/165.232.139.40" +# target: "/tmp" +# state: present + +# - name: Unmount specific directory +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# target: "/tmp" +# state: absent + +# - name: Unmount nonexistent mount point +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# target: "/tmpxx" +# state: absent + +# - name: Unmount all directories +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# state: absent + +# - name: Unmount all directories (idempotency check) +# theko2fi.multipass.multipass_mount: +# name: "healthy-cankerworm" +# state: absent + diff --git a/tests/integration/targets/multipass_vm/tasks/main.yml b/tests/integration/targets/multipass_vm/tasks/main.yml index 82a54d7..7ad38ba 100644 --- a/tests/integration/targets/multipass_vm/tasks/main.yml +++ b/tests/integration/targets/multipass_vm/tasks/main.yml @@ -1,4 +1,9 @@ --- +- name: Ensure multipass VM named zazilapus doesn't exist + theko2fi.multipass.multipass_vm: + name: "zazilapus" + state: absent + purge: true - name: Create a multipass VM theko2fi.multipass.multipass_vm: @@ -129,4 +134,7 @@ - name: Verify that VM purge is idempotent ansible.builtin.assert: that: - - repurge_vm.changed == False \ No newline at end of file + - repurge_vm.changed == False + +- name: Include mount tests + ansible.builtin.import_tasks: mount.yml \ No newline at end of file diff --git a/tests/integration/targets/multipass_vm/tasks/mount.yml b/tests/integration/targets/multipass_vm/tasks/mount.yml new file mode 100644 index 0000000..d747798 --- /dev/null +++ b/tests/integration/targets/multipass_vm/tasks/mount.yml @@ -0,0 +1,126 @@ +--- + +- name: Ensure that VM doesn't exist + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: absent + purge: true + +- name: Ensure that mount source folder exists + ansible.builtin.file: + path: "/root/tmp/testmount2" + state: directory + +- name: Mount directories to multipass VM + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/root/tmp/testmount2" + target: "/tmpyy" + gid_map: ["50:50","500:25"] + uid_map: ["1000:1000"] + - source: "/tmp" + target: "/tmpxx" + register: create_vm_with_mounts + +- name: Verify that VM has been created with mounts + ansible.builtin.assert: + that: + - create_vm_with_mounts.changed + - "'/tmpxx' in create_vm_with_mounts.result.info.zobosky.mounts" + - "'/tmpyy' in create_vm_with_mounts.result.info.zobosky.mounts" + - create_vm_with_mounts.result.info.zobosky.mounts['/tmpyy'].gid_mappings == ["50:50","500:25"] + - create_vm_with_mounts.result.info.zobosky.mounts['/tmpyy'].uid_mappings == ["1000:1000"] + +- name: Reduce gid_map + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/root/tmp/testmount2" + target: "/tmpyy" + gid_map: ["50:50"] + uid_map: ["1000:1000"] + - source: "/tmp" + target: "/tmpxx" + register: reduce_gid_map + +- name: Verify that gid_map reduced + ansible.builtin.assert: + that: + - reduce_gid_map.changed + - reduce_gid_map.result.info.zobosky.mounts['/tmpyy'].gid_mappings == ["50:50"] + +- name: Remove uid_map + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/root/tmp/testmount2" + target: "/tmpyy" + gid_map: ["50:50"] + - source: "/tmp" + target: "/tmpxx" + register: remove_uid_map + +- name: Verify that uid_map reverted to default + ansible.builtin.assert: + that: + - remove_uid_map.changed + - remove_uid_map.result.info.zobosky.mounts['/tmpyy'].uid_mappings == ["0:default"] + +- name: change '/tmpxx' mount point to '/tmpzz' + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/root/tmp/testmount2" + target: "/tmpyy" + gid_map: ["50:50"] + - source: "/tmp" + target: "/tmpzz" + register: change_mount_point + +- name: Verify that '/tmpxx' changed to '/tmpzz' + ansible.builtin.assert: + that: + - change_mount_point.changed + - "'/tmpxx' not in change_mount_point.result.info.zobosky.mounts" + - change_mount_point.result.info.zobosky.mounts['/tmpzz'].source_path == "/tmp" + +- name: Remove one mount point + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/tmp" + target: "/tmpzz" + register: remove_one_mount_point + +- name: Verify that '/tmpyy' mount point has been removed + ansible.builtin.assert: + that: + - remove_one_mount_point.changed + - "'/tmpyy' not in remove_one_mount_point.result.info.zobosky.mounts" + +- name: Remove one mount point (check idempotency) + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: present + mounts: + - source: "/tmp" + target: "/tmpzz" + register: remove_one_mount_point_idempotency + + +- name: Verify that '/tmpyy' mount point has been removed (idempotency) + ansible.builtin.assert: + that: + - remove_one_mount_point_idempotency.changed == False + +- name: Delete and purge VM + theko2fi.multipass.multipass_vm: + name: "zobosky" + state: absent + purge: true \ No newline at end of file From bbe5cb91c24a4e6242fbacd1d20dd2df343db1da Mon Sep 17 00:00:00 2001 From: theko2fi <72862222+theko2fi@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:11:08 +0100 Subject: [PATCH 2/8] Use Matrix in CI/CD pipeline (#12) --- .github/files/user-data.yml | 1 - .github/workflows/cicd.yml | 43 +++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/files/user-data.yml b/.github/files/user-data.yml index b6f456e..78b91e4 100644 --- a/.github/files/user-data.yml +++ b/.github/files/user-data.yml @@ -5,5 +5,4 @@ runcmd: - apt update - apt install python3-pip -y - pip3 install ansible - - pip3 install git+https://github.com/theko2fi/multipass-python-sdk.git - snap install multipass \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 4c93b9e..cde04a2 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -8,8 +8,10 @@ on: pull_request: jobs: - test: + create_droplet: runs-on: ubuntu-latest + outputs: + DROPLET_IP: ${{ steps.droplet-creator.outputs.DROPLET_IP }} steps: - name: Fetch the repo uses: actions/checkout@v3 @@ -23,7 +25,24 @@ jobs: id: droplet-creator run: | DROPLET_IP=$(doctl compute droplet create --image ubuntu-22-04-x64 --size s-4vcpu-8gb --region nyc1 --user-data-file .github/files/user-data.yml --ssh-keys ${{ secrets.SSH_KEY_ID }} --wait --output json ansible-multipass-droplet | jq -r '.[].networks.v4[] | select(.type == "public") | .ip_address') - echo "DROPLET_IP=${DROPLET_IP}" >> "$GITHUB_ENV" + echo "DROPLET_IP=${DROPLET_IP}" >> "$GITHUB_OUTPUT" + + ansible_test: + runs-on: ubuntu-latest + needs: [create_droplet] + strategy: + matrix: + target: + - multipass_vm + - multipass_vm_exec + - multipass_vm_info + - multipass_vm_purge + - multipass_vm_transfer_into + exclude: + - target: connection_multipass + steps: + - name: Fetch the repo + uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v4 @@ -50,7 +69,7 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 30 max_attempts: 3 - command: ssh-keyscan -4 -t rsa -p 22 -T 240 ${{ env.DROPLET_IP }} >> ~/.ssh/known_hosts + command: ssh-keyscan -4 -t rsa -p 22 -T 240 ${{ needs.create_droplet.outputs.DROPLET_IP }} >> ~/.ssh/known_hosts - name: check cloud-init status uses: nick-fields/retry@v2 @@ -58,13 +77,25 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 60 max_attempts: 5 - command: ssh ${{ secrets.SSH_USERNAME }}@${{ env.DROPLET_IP }} 'cloud-init status' | grep -q "done" + command: ssh ${{ secrets.SSH_USERNAME }}@${{ needs.create_droplet.outputs.DROPLET_IP }} 'cloud-init status' | grep -q "done" - name: Run ansible-test run: | cd ~/.ansible/collections/ansible_collections/theko2fi/multipass - ansible-test integration --target ssh:${{ secrets.SSH_USERNAME }}@${{ env.DROPLET_IP }},python=3.10 --exclude connection_multipass + ansible-test integration --target ssh:${{ secrets.SSH_USERNAME }}@${{ needs.create_droplet.outputs.DROPLET_IP }},python=3.10 ${{ matrix.target }} + + destroy_droplet: + runs-on: ubuntu-latest + needs: [ansible_test] + if: ${{ always() }} + steps: + - name: Fetch the repo + uses: actions/checkout@v3 + + - name: Install doctl + uses: digitalocean/action-doctl@v2 + with: + token: ${{ secrets.DIGITALOCEAN_API_TOKEN }} - name: Delete droplet - if: ${{ always() }} run: doctl compute droplet delete ansible-multipass-droplet --force \ No newline at end of file From 380876c5e994e1bf530e123d772731e2f40b4272 Mon Sep 17 00:00:00 2001 From: theko2fi Date: Mon, 12 Feb 2024 15:37:33 +0100 Subject: [PATCH 3/8] Revert "Use Matrix in CI/CD pipeline (#12)" This reverts commit bbe5cb91c24a4e6242fbacd1d20dd2df343db1da. --- .github/files/user-data.yml | 1 + .github/workflows/cicd.yml | 43 ++++++------------------------------- 2 files changed, 7 insertions(+), 37 deletions(-) diff --git a/.github/files/user-data.yml b/.github/files/user-data.yml index 78b91e4..b6f456e 100644 --- a/.github/files/user-data.yml +++ b/.github/files/user-data.yml @@ -5,4 +5,5 @@ runcmd: - apt update - apt install python3-pip -y - pip3 install ansible + - pip3 install git+https://github.com/theko2fi/multipass-python-sdk.git - snap install multipass \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index cde04a2..4c93b9e 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -8,10 +8,8 @@ on: pull_request: jobs: - create_droplet: + test: runs-on: ubuntu-latest - outputs: - DROPLET_IP: ${{ steps.droplet-creator.outputs.DROPLET_IP }} steps: - name: Fetch the repo uses: actions/checkout@v3 @@ -25,24 +23,7 @@ jobs: id: droplet-creator run: | DROPLET_IP=$(doctl compute droplet create --image ubuntu-22-04-x64 --size s-4vcpu-8gb --region nyc1 --user-data-file .github/files/user-data.yml --ssh-keys ${{ secrets.SSH_KEY_ID }} --wait --output json ansible-multipass-droplet | jq -r '.[].networks.v4[] | select(.type == "public") | .ip_address') - echo "DROPLET_IP=${DROPLET_IP}" >> "$GITHUB_OUTPUT" - - ansible_test: - runs-on: ubuntu-latest - needs: [create_droplet] - strategy: - matrix: - target: - - multipass_vm - - multipass_vm_exec - - multipass_vm_info - - multipass_vm_purge - - multipass_vm_transfer_into - exclude: - - target: connection_multipass - steps: - - name: Fetch the repo - uses: actions/checkout@v3 + echo "DROPLET_IP=${DROPLET_IP}" >> "$GITHUB_ENV" - name: Set up Python 3.10 uses: actions/setup-python@v4 @@ -69,7 +50,7 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 30 max_attempts: 3 - command: ssh-keyscan -4 -t rsa -p 22 -T 240 ${{ needs.create_droplet.outputs.DROPLET_IP }} >> ~/.ssh/known_hosts + command: ssh-keyscan -4 -t rsa -p 22 -T 240 ${{ env.DROPLET_IP }} >> ~/.ssh/known_hosts - name: check cloud-init status uses: nick-fields/retry@v2 @@ -77,25 +58,13 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 60 max_attempts: 5 - command: ssh ${{ secrets.SSH_USERNAME }}@${{ needs.create_droplet.outputs.DROPLET_IP }} 'cloud-init status' | grep -q "done" + command: ssh ${{ secrets.SSH_USERNAME }}@${{ env.DROPLET_IP }} 'cloud-init status' | grep -q "done" - name: Run ansible-test run: | cd ~/.ansible/collections/ansible_collections/theko2fi/multipass - ansible-test integration --target ssh:${{ secrets.SSH_USERNAME }}@${{ needs.create_droplet.outputs.DROPLET_IP }},python=3.10 ${{ matrix.target }} - - destroy_droplet: - runs-on: ubuntu-latest - needs: [ansible_test] - if: ${{ always() }} - steps: - - name: Fetch the repo - uses: actions/checkout@v3 - - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_API_TOKEN }} + ansible-test integration --target ssh:${{ secrets.SSH_USERNAME }}@${{ env.DROPLET_IP }},python=3.10 --exclude connection_multipass - name: Delete droplet + if: ${{ always() }} run: doctl compute droplet delete ansible-multipass-droplet --force \ No newline at end of file From f372b43ddd83d6be6ea760609972519c4219bd77 Mon Sep 17 00:00:00 2001 From: theko2fi Date: Thu, 29 Feb 2024 21:37:03 +0100 Subject: [PATCH 4/8] List roles in `Included content` section of readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 01bacc1..3590f56 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Please note that this collection is **not** developed by [Canonical](https://can - [multipass_vm_transfer_into](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_vm_transfer_into_module.html) - Module to copy a file into a Multipass virtual machine - [multipass_config_get](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_config_get_module.html) - Module to get Multipass configuration setting - [multipass_mount](https://theko2fi.github.io/ansible-multipass-collection/branch/main/multipass_mount_module.html) - Module to manage directory mapping between host and Multipass virtual machines +* Roles: + - molecule_multipass - Molecule Multipass driver ## Installation From 470cfd8ed9c1fb45b10615adb0d9420d79717d6a Mon Sep 17 00:00:00 2001 From: theko2fi Date: Thu, 29 Feb 2024 21:50:39 +0100 Subject: [PATCH 5/8] added umami analytic --- .github/workflows/docs-push.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs-push.yml b/.github/workflows/docs-push.yml index 493e4d0..53662ca 100644 --- a/.github/workflows/docs-push.yml +++ b/.github/workflows/docs-push.yml @@ -37,6 +37,7 @@ init-html-short-title: Theko2fi.Multipass Collection Docs init-extra-html-theme-options: | documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/ + init-append-conf-py: "html_js_files=[('https://eu.umami.is/script.js', {'data-website-id': '0086c656-f41e-4131-ac3f-59bf72b1c4d8','defer': 'defer'})]" publish-docs-gh-pages: # for now we won't run this on forks From 44eab8f916be6bbbb635781922074c276d9326d0 Mon Sep 17 00:00:00 2001 From: theko2fi Date: Thu, 29 Feb 2024 22:53:43 +0100 Subject: [PATCH 6/8] set version to 0.3.0 --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index 8870de6..d7fea7d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,7 +8,7 @@ namespace: theko2fi name: multipass # The version of the collection. Must be compatible with semantic versioning -version: 0.2.3 +version: 0.3.0 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md From 1dc0c4d11dde0dabb694b2522dba70f114261e33 Mon Sep 17 00:00:00 2001 From: theko2fi Date: Thu, 29 Feb 2024 22:54:10 +0100 Subject: [PATCH 7/8] changelog v0.3.0 --- CHANGELOG.rst | 21 +++++++++++++++++++++ changelogs/changelog.yaml | 20 ++++++++++++++++++++ changelogs/fragments/v0.3.0.yaml | 6 ++++++ 3 files changed, 47 insertions(+) create mode 100644 changelogs/fragments/v0.3.0.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe7f908..839a41f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,27 @@ Theko2Fi.Multipass Release Notes .. contents:: Topics +v0.3.0 +====== + +Release Summary +--------------- + +Release Date: 2024-02-29 + +The collection now contains module and option to manage directory mapping between host and Multipass virtual machines. + + +Minor Changes +------------- + +- multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. + +New Modules +----------- + +- theko2fi.multipass.multipass_mount - Module to manage directory mapping between host and Multipass virtual machine + v0.2.3 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 2ce5dbe..81c497f 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -70,3 +70,23 @@ releases: fragments: - v0.2.3.yaml release_date: '2024-01-05' + 0.3.0: + changes: + minor_changes: + - multipass_vm - add ``mount`` option which allows to mount host directories + inside multipass instances. + release_summary: 'Release Date: 2024-02-29 + + + The collection now contains module and option to manage directory mapping + between host and Multipass virtual machines. + + ' + fragments: + - v0.3.0.yaml + modules: + - description: Module to manage directory mapping between host and Multipass virtual + machine + name: multipass_mount + namespace: '' + release_date: '2024-02-29' diff --git a/changelogs/fragments/v0.3.0.yaml b/changelogs/fragments/v0.3.0.yaml new file mode 100644 index 0000000..022460a --- /dev/null +++ b/changelogs/fragments/v0.3.0.yaml @@ -0,0 +1,6 @@ +release_summary: | + Release Date: 2024-02-29 + + The collection now contains module and option to manage directory mapping between host and Multipass virtual machines. +minor_changes: + - multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. \ No newline at end of file From 140be9a27845cca8e98b205a8cd7e855bc928958 Mon Sep 17 00:00:00 2001 From: theko2fi Date: Fri, 1 Mar 2024 11:27:28 +0100 Subject: [PATCH 8/8] add molecule_multipass to changelog --- CHANGELOG.rst | 4 ++++ changelogs/changelog.yaml | 8 +++++++- changelogs/fragments/v0.3.0.yaml | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 839a41f..c310f1d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,13 @@ Release Date: 2024-02-29 The collection now contains module and option to manage directory mapping between host and Multipass virtual machines. +It also contains a Multipass driver for Molecule which allow to use Multipass instances for provisioning test resources. + + Minor Changes ------------- +- molecule_multipass - a Multipass driver for Molecule. - multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. New Modules diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 81c497f..cc1b73a 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -73,6 +73,7 @@ releases: 0.3.0: changes: minor_changes: + - molecule_multipass - a Multipass driver for Molecule. - multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. release_summary: 'Release Date: 2024-02-29 @@ -81,6 +82,11 @@ releases: The collection now contains module and option to manage directory mapping between host and Multipass virtual machines. + + + It also contains a Multipass driver for Molecule which allow to use Multipass + instances for provisioning test resources. + ' fragments: - v0.3.0.yaml @@ -89,4 +95,4 @@ releases: machine name: multipass_mount namespace: '' - release_date: '2024-02-29' + release_date: '2024-03-01' diff --git a/changelogs/fragments/v0.3.0.yaml b/changelogs/fragments/v0.3.0.yaml index 022460a..95f5c7d 100644 --- a/changelogs/fragments/v0.3.0.yaml +++ b/changelogs/fragments/v0.3.0.yaml @@ -2,5 +2,9 @@ release_summary: | Release Date: 2024-02-29 The collection now contains module and option to manage directory mapping between host and Multipass virtual machines. + + + It also contains a Multipass driver for Molecule which allow to use Multipass instances for provisioning test resources. minor_changes: - - multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. \ No newline at end of file + - multipass_vm - add ``mount`` option which allows to mount host directories inside multipass instances. + - molecule_multipass - a Multipass driver for Molecule. \ No newline at end of file