From 23a38e1963a5e9478d6878669e5094ae25828004 Mon Sep 17 00:00:00 2001 From: Peter Magnusson Date: Fri, 29 Nov 2024 15:16:54 +0100 Subject: [PATCH] tests: basic unit testing on incus_instance (#17) - includes crude mocking of incuscli --- DEVELOP.md | 6 + Makefile | 24 +++ .../targets/incus_instance/tasks/main.yaml | 22 ++- tests/unit/plugins/modules/SAMPLE_OUTPUT.md | 172 ++++++++++++++++++ tests/unit/plugins/modules/clients.py | 145 +++++++++++++++ .../modules/fixtures/get_instance.json | 74 ++++++++ .../modules/fixtures/get_instance_state.json | 28 +++ .../fixtures/get_instance_state_running.json | 97 ++++++++++ .../modules/fixtures/post_instance.json | 33 ++++ .../modules/fixtures/put_instance_state.json | 26 +++ .../plugins/modules/fixtures/response.json | 1 + .../plugins/modules/test_incus_instance.py | 89 +++++++++ 12 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 tests/unit/plugins/modules/SAMPLE_OUTPUT.md create mode 100644 tests/unit/plugins/modules/clients.py create mode 100644 tests/unit/plugins/modules/fixtures/get_instance.json create mode 100644 tests/unit/plugins/modules/fixtures/get_instance_state.json create mode 100644 tests/unit/plugins/modules/fixtures/get_instance_state_running.json create mode 100644 tests/unit/plugins/modules/fixtures/post_instance.json create mode 100644 tests/unit/plugins/modules/fixtures/put_instance_state.json create mode 100644 tests/unit/plugins/modules/fixtures/response.json create mode 100644 tests/unit/plugins/modules/test_incus_instance.py diff --git a/DEVELOP.md b/DEVELOP.md index 9597904..afa512c 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -67,7 +67,13 @@ ansible-test sanity --python 3.11 -v # test with local venv ansible-test units --venv --python 3.11 +# with docker +ansible-test units --docker -v + +# run integrations tests on your local incus installation ansible-test integration --venv --python 3.11 unsupported/incus_instance + + ``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57f1707 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ + +.PHONY: clean +clean: + @echo "Cleaning up..." + @echo "Remove tests/output directories" + rm -rf tests/output + + @echo "Remove all python related directories" + find . -type d -name __pycache__ -exec rm -rf {} \; + rm -rf .mypy_cache + + +.PHONY: test +test: test/sanity test/units + + +.PHONY: test/sanity +test/sanity: + ansible-test sanity --python 3.11 -v + + +.PHONY: test/units +test/units: + ansible-test units --venv --python 3.11 diff --git a/tests/integration/targets/incus_instance/tasks/main.yaml b/tests/integration/targets/incus_instance/tasks/main.yaml index 9e2e7c9..04c1b84 100644 --- a/tests/integration/targets/incus_instance/tasks/main.yaml +++ b/tests/integration/targets/incus_instance/tasks/main.yaml @@ -5,7 +5,7 @@ source: type: image alias: debian/12/cloud - server: "https://images.incus.org" + server: "https://images.linuxcontainers.org" protocol: "simplestreams" mode: "pull" allow_inconsistent: false @@ -17,6 +17,19 @@ state: absent register: destroy_result +# - name: create vm +# kmpm.incus.incus_instance: +# name: myvm +# type: virtual-machine +# source: +# type: image +# alias: debian/12/cloud +# server: "https://images.linuxcontainers.org" +# protocol: "simplestreams" +# mode: "pull" +# allow_inconsistent: false +# register: create_vm_result + - name: check create_result assert: that: @@ -30,3 +43,10 @@ - destroy_result.changed == True - destroy_result.actions == ['stop', 'delete'] - not destroy_result.failed + +# - name: check create_vm_result +# assert: +# that: +# - create_vm_result.changed == True +# - create_vm_result.actions == ['create', 'start'] +# - not create_vm_result.failed diff --git a/tests/unit/plugins/modules/SAMPLE_OUTPUT.md b/tests/unit/plugins/modules/SAMPLE_OUTPUT.md new file mode 100644 index 0000000..b2e1552 --- /dev/null +++ b/tests/unit/plugins/modules/SAMPLE_OUTPUT.md @@ -0,0 +1,172 @@ +# Sample responses + +This document lists sample responses using `incus query` for different tasks and +is intended to help out during development. + +## POST instance response + +Creating a container instance called `testinstance`. + +```json +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "id": "a252b7bd-7178-42ca-8d94-1680254eec7b", + "class": "task", + "description": "Creating instance", + "created_at": "2024-11-28T23:11:49.303545747+01:00", + "updated_at": "2024-11-28T23:11:50.724932671+01:00", + "status": "Success", + "status_code": 200, + "resources": { + "instances": [ + "/1.0/instances/testinstance" + ] + }, + "metadata": { + "create_instance_from_image_unpack_progress": "Unpacking image: 100% (3.97GB/s)", + "progress": { + "percent": "100", + "speed": "3972972972", + "stage": "create_instance_from_image_unpack" + } + }, + "may_cancel": false, + "err": "", + "location": "none" + } +} +``` + +## Get absent instance + +```json +{ + "type": "error", + "status":"", + "status_code": 0, + "operation":"", + "error_code":404, + "error":"Not Found", + "metadata":null +} +``` + +## GET state + +Using the path `/1.0/instances/testinstance/state?project=default` + +```json +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "status": "Stopped", + "status_code": 102, + "disk": {}, + "memory": { + "usage": 0, + "usage_peak": 0, + "total": 0, + "swap_usage": 0, + "swap_usage_peak": 0 + }, + "network": null, + "pid": 0, + "processes": 0, + "cpu": { + "usage": 0 + }, + "started_at": "0001-01-01T00:00:00Z", + "os_info": null + } +} +``` + +## Get existing container + +```json +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "architecture": "aarch64", + "config": { + "image.architecture": "arm64", + "image.description": "Debian bookworm arm64 (20241128_05:24)", + "image.os": "Debian", + "image.release": "bookworm", + "image.serial": "20241128_05:24", + "image.type": "squashfs", + "image.variant": "cloud", + "volatile.apply_template": "create", + "volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196", + "volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1", + "volatile.eth0.hwaddr": "00:16:3e:9a:00:37", + "volatile.idmap.base": "0", + "volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]", + "volatile.last_state.idmap": "[]", + "volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0", + "volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0" + }, + "devices": {}, + "ephemeral": false, + "profiles": [ + "default" + ], + "stateful": false, + "description": "", + "created_at": "2024-11-28T22:11:50.701255415Z", + "expanded_config": { + "image.architecture": "arm64", + "image.description": "Debian bookworm arm64 (20241128_05:24)", + "image.os": "Debian", + "image.release": "bookworm", + "image.serial": "20241128_05:24", + "image.type": "squashfs", + "image.variant": "cloud", + "volatile.apply_template": "create", + "volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196", + "volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1", + "volatile.eth0.hwaddr": "00:16:3e:9a:00:37", + "volatile.idmap.base": "0", + "volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]", + "volatile.last_state.idmap": "[]", + "volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0", + "volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0" + }, + "expanded_devices": { + "eth0": { + "name": "eth0", + "network": "incusbr0", + "type": "nic" + }, + "root": { + "path": "/", + "pool": "default", + "type": "disk" + } + }, + "name": "testinstance", + "status": "Stopped", + "status_code": 102, + "last_used_at": "1970-01-01T00:00:00Z", + "location": "none", + "type": "container", + "project": "default" + } +} +``` diff --git a/tests/unit/plugins/modules/clients.py b/tests/unit/plugins/modules/clients.py new file mode 100644 index 0000000..dce8d04 --- /dev/null +++ b/tests/unit/plugins/modules/clients.py @@ -0,0 +1,145 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import os +from ansible.module_utils.six.moves.urllib.parse import urlparse, parse_qs + + +fixturedir = os.path.join(os.path.dirname(__file__), 'fixtures') +status_messages = { + 0: '', + 200: 'Success', +} + +error_messages = { + 0: '', + 404: 'Not Found', + 500: 'Internal Server Error', + 501: 'Not Implemented', +} + + +def load_fixture(name): + with open(os.path.join(fixturedir, name)) as f: + return json.load(f) + + +def generate_response(**kwargs): + resp = { + 'type': 'sync', + 'status': '', + 'status_code': 0, + 'operation': '', + 'error_code': 0, + 'error': '', + 'metadata': None + } + for k, v in kwargs.items(): + if k in resp: + resp[k] = v + + resp['status'] = status_messages.get(resp['status_code'], '') + if resp['error_code']: + resp['error'] = error_messages.get(resp['error_code'], '') + resp['type'] = 'error' + + return resp + + +class MockClient: + instances = {} + counter = 0 + request = {} + + def _response(self, value): + stringval = json.dumps(value) + print("response", dict(requst=self.request, response=stringval)) + return stringval + + def _instances_api(self, method, segments, queryargs, data): + name = segments[0] if len(segments) > 0 else None + if not name: + if method == 'GET': + return json.dumps([]) + elif method == 'POST': + # create instance + name = data['name'] + metadata = load_fixture('get_instance.json')['metadata'] + metadata['name'] = name + metadata['project'] = queryargs.get('project', ['default'])[0] + self.instances[name] = metadata + resp = load_fixture('post_instance.json') + self.counter += 1 + resp['metadata']['id'] = '1337-42-%d' % self.counter + resp['metadata']['name'] = metadata['name'] + resp['metadata']['project'] = metadata['project'] + # print("creates instance", name, resp['metadata']) + return self._response(resp) + + segments = segments[1:] + if segments and segments[0] == 'state': + segments = segments[1:] + if method == 'GET': + # return named instance state if any + if name in self.instances: + resp = load_fixture('get_instance_state.json') + metadata = self.instances[name] + resp['metadata'] = self.instances[name]['state'] + return self._response(resp) + else: + return json.dumps(generate_response(error_code=404)) + elif method == 'PUT': + # update state + if name in self.instances: + resp = load_fixture('put_instance_state.json') + self.instances[name]['status'] = "Running" + self.instances[name]['status_code'] = "103" + + return self._response(resp) + else: + return self._response(generate_response(error_code=404)) + else: + if method == 'GET': + # return named instance if any + if name in self.instances: + resp = generate_response(status_code=200) + resp['metadata'] = self.instances[name] + return self._response(resp) + else: + return self._response(generate_response(error_code=404)) + + raise Exception('instance action: %s %r' % (method, segments,)) + + def execute(self, client, *args, **kwargs): + # if client.debug: + client.debug = True + client.logs.append(args) + method = args[2] + querypath = args[3] + # extract queryargs from querypath + p = urlparse(querypath) + queryargs = parse_qs(p.query) + data = None + for i, arg in enumerate(args): + if arg == '--data': + data = json.loads(args[i + 1]) + + self.request = dict(path=querypath, data=data) + # extract segments from querypath + segments = p.path.split('/') + # remove empty first segment + segments = segments[1:] + # is the first segment '1.0'? + if segments[0] != '1.0': + raise Exception('Unknown version: %s' % segments) + + # remove version segment + segments = segments[1:] + + if segments[0] == 'instances': + return self._instances_api(method, segments[1:], queryargs, data) + + else: + print("log", client.logs) + raise Exception('Unknown method or querypath: %s %s' % (method, querypath,)) diff --git a/tests/unit/plugins/modules/fixtures/get_instance.json b/tests/unit/plugins/modules/fixtures/get_instance.json new file mode 100644 index 0000000..b07b507 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/get_instance.json @@ -0,0 +1,74 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "architecture": "aarch64", + "config": { + "image.architecture": "arm64", + "image.description": "Debian bookworm arm64 (20241128_05:24)", + "image.os": "Debian", + "image.release": "bookworm", + "image.serial": "20241128_05:24", + "image.type": "squashfs", + "image.variant": "cloud", + "volatile.apply_template": "create", + "volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196", + "volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1", + "volatile.eth0.hwaddr": "00:16:3e:9a:00:37", + "volatile.idmap.base": "0", + "volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]", + "volatile.last_state.idmap": "[]", + "volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0", + "volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0" + }, + "devices": {}, + "ephemeral": false, + "profiles": [ + "default" + ], + "stateful": false, + "description": "", + "created_at": "2024-11-28T22:11:50.701255415Z", + "expanded_config": { + "image.architecture": "arm64", + "image.description": "Debian bookworm arm64 (20241128_05:24)", + "image.os": "Debian", + "image.release": "bookworm", + "image.serial": "20241128_05:24", + "image.type": "squashfs", + "image.variant": "cloud", + "volatile.apply_template": "create", + "volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196", + "volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1", + "volatile.eth0.hwaddr": "00:16:3e:9a:00:37", + "volatile.idmap.base": "0", + "volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]", + "volatile.last_state.idmap": "[]", + "volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0", + "volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0" + }, + "expanded_devices": { + "eth0": { + "name": "eth0", + "network": "incusbr0", + "type": "nic" + }, + "root": { + "path": "/", + "pool": "default", + "type": "disk" + } + }, + "name": "", + "status": "Stopped", + "status_code": 102, + "last_used_at": "1970-01-01T00:00:00Z", + "location": "none", + "type": "", + "project": "" + } +} diff --git a/tests/unit/plugins/modules/fixtures/get_instance_state.json b/tests/unit/plugins/modules/fixtures/get_instance_state.json new file mode 100644 index 0000000..da9e2b1 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/get_instance_state.json @@ -0,0 +1,28 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "status": "Stopped", + "status_code": 102, + "disk": {}, + "memory": { + "usage": 0, + "usage_peak": 0, + "total": 0, + "swap_usage": 0, + "swap_usage_peak": 0 + }, + "network": null, + "pid": 0, + "processes": 0, + "cpu": { + "usage": 0 + }, + "started_at": "0001-01-01T00:00:00Z", + "os_info": null + } +} diff --git a/tests/unit/plugins/modules/fixtures/get_instance_state_running.json b/tests/unit/plugins/modules/fixtures/get_instance_state_running.json new file mode 100644 index 0000000..5f94035 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/get_instance_state_running.json @@ -0,0 +1,97 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "status": "Running", + "status_code": 103, + "disk": {}, + "memory": { + "usage": 0, + "usage_peak": 0, + "total": 0, + "swap_usage": 0, + "swap_usage_peak": 0 + }, + "network": { + "eth0": { + "addresses": [ + { + "family": "inet", + "address": "10.201.82.115", + "netmask": "24", + "scope": "global" + }, + { + "family": "inet6", + "address": "fd42:9ad6:a276:908a:216:3eff:fe9a:37", + "netmask": "64", + "scope": "global" + }, + { + "family": "inet6", + "address": "fe80::216:3eff:fe9a:37", + "netmask": "64", + "scope": "link" + } + ], + "counters": { + "bytes_received": 2938, + "bytes_sent": 3876, + "packets_received": 21, + "packets_sent": 37, + "errors_received": 0, + "errors_sent": 0, + "packets_dropped_outbound": 0, + "packets_dropped_inbound": 0 + }, + "hwaddr": "00:16:3e:9a:00:37", + "host_name": "vetha826f1a0", + "mtu": 1500, + "state": "up", + "type": "broadcast" + }, + "lo": { + "addresses": [ + { + "family": "inet", + "address": "127.0.0.1", + "netmask": "8", + "scope": "local" + }, + { + "family": "inet6", + "address": "::1", + "netmask": "128", + "scope": "local" + } + ], + "counters": { + "bytes_received": 0, + "bytes_sent": 0, + "packets_received": 0, + "packets_sent": 0, + "errors_received": 0, + "errors_sent": 0, + "packets_dropped_outbound": 0, + "packets_dropped_inbound": 0 + }, + "hwaddr": "", + "host_name": "", + "mtu": 65536, + "state": "up", + "type": "loopback" + } + }, + "pid": 420867, + "processes": 8, + "cpu": { + "usage": 6305951000 + }, + "started_at": "2024-11-29T13:58:31.241279183+01:00", + "os_info": null + } +} diff --git a/tests/unit/plugins/modules/fixtures/post_instance.json b/tests/unit/plugins/modules/fixtures/post_instance.json new file mode 100644 index 0000000..451983a --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/post_instance.json @@ -0,0 +1,33 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "id": "", + "class": "task", + "description": "Creating instance", + "created_at": "2024-11-28T23:11:49.303545747+01:00", + "updated_at": "2024-11-28T23:11:50.724932671+01:00", + "status": "Success", + "status_code": 200, + "resources": { + "instances": [ + "/1.0/instances/testinstance" + ] + }, + "metadata": { + "create_instance_from_image_unpack_progress": "Unpacking image: 100% (3.97GB/s)", + "progress": { + "percent": "100", + "speed": "3972972972", + "stage": "create_instance_from_image_unpack" + } + }, + "may_cancel": false, + "err": "", + "location": "none" + } +} diff --git a/tests/unit/plugins/modules/fixtures/put_instance_state.json b/tests/unit/plugins/modules/fixtures/put_instance_state.json new file mode 100644 index 0000000..8353a13 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/put_instance_state.json @@ -0,0 +1,26 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "id": "b9958a0e-842f-4a7c-8b30-14b9b4065cf0", + "class": "task", + "description": "Starting instance", + "created_at": "2024-11-29T13:58:31.07329825+01:00", + "updated_at": "2024-11-29T13:58:31.07329825+01:00", + "status": "Success", + "status_code": 200, + "resources": { + "instances": [ + "/1.0/instances/testinstance" + ] + }, + "metadata": null, + "may_cancel": false, + "err": "", + "location": "none" + } +} diff --git a/tests/unit/plugins/modules/fixtures/response.json b/tests/unit/plugins/modules/fixtures/response.json new file mode 100644 index 0000000..995cb83 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/response.json @@ -0,0 +1 @@ +{"type": "sync", "status":"", "status_code": 0, "operation":"", "metadata":null} diff --git a/tests/unit/plugins/modules/test_incus_instance.py b/tests/unit/plugins/modules/test_incus_instance.py new file mode 100644 index 0000000..c118739 --- /dev/null +++ b/tests/unit/plugins/modules/test_incus_instance.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Peter Magnusson +# 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 + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.community.general.tests.unit.compat.mock import patch +from ansible_collections.kmpm.incus.plugins.modules import incus_instance +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase, set_module_args + +from .clients import MockClient + +mcli = MockClient() + + +def mock_execute(client, *args, **kwargs): + return mcli.execute(client, *args, **kwargs) + + +def fake_bin_path(arg1): + return '/usr/bin/incus' + + +@patch("ansible_collections.kmpm.incus.plugins.module_utils.incuscli.get_bin_path", fake_bin_path) +@patch('ansible_collections.kmpm.incus.plugins.modules.incus_instance.IncusClient._execute', mock_execute) +class IncusInstanceTestCase(ModuleTestCase): + module = incus_instance + + def setUp(self): + super(IncusInstanceTestCase, self).setUp() + ansible_module_path = 'ansible_collections.kmpm.incus.plugins.modules.incus_instance.AnsibleModule' + self.mock_run_command = patch('%s.run_command' % ansible_module_path) + self.module_main_command = self.mock_run_command.start() + + def tearDown(self): + self.mock_run_command.stop() + super(IncusInstanceTestCase, self).tearDown() + + def module_main(self, exit_exc): + with self.assertRaises(exit_exc) as exc: + self.module.main() + return exc.exception.args[0] + + def test_absent(self): + set_module_args({'name': 'testinstance', 'state': 'absent'}) + self.module_main_command.side_effect = [ + (0, '{}', ''), + (0, '{}', ''), + ] + result = self.module_main(AnsibleExitJson) + self.assertFalse(result['changed'], result) + + def test_started_container(self): + set_module_args({ + 'name': 'testinstance', + 'state': 'started', + 'source': { + 'type': 'image', + 'alias': 'debian/12/cloud', + 'server': "https://images.linuxcontainers.org", + 'protocol': "simplestreams", + 'mode': "pull" + } + }) + self.module_main_command.side_effect = [ + (0, '{}', ''), + (0, '{}', ''), + ] + result = self.module_main(AnsibleExitJson) + print("result", result) + self.assertTrue(result['changed'], result) + + # def test_started_vm(self): + # set_module_args({ + # 'name': 'testvm', + # 'state': 'started', + # 'source': { + # 'type': 'image', + # 'alias': 'debian/12/cloud', + # 'server': "https://images.linuxcontainers.org", + # 'protocol': "simplestreams", + # 'mode': "pull", + # }, + # 'type': 'virtual-machine', + # })