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