Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.idea
**__pycache__
.coverage
24 changes: 22 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,25 @@ include:
project: QubesOS/qubes-continuous-integration
- file: /r4.3/gitlab-host.yml
project: QubesOS/qubes-continuous-integration
- file: /r4.3/gitlab-vm.yml
project: QubesOS/qubes-continuous-integration

.mgmt-host-template:
stage: tests
tags:
- vm-kvm
after_script:
- ci/codecov-wrapper -F unittests
before_script:
# install dependencies
- sudo qubes-dom0-update -y ansible python3-pytest python3-coverage perl-Digest-SHA
# install from artifacts
- find $CI_PROJECT_DIR/artifacts/repository -name '*.noarch.rpm' -exec sudo dnf install -y {} \+
script:
# run ansible's tests
- cd /usr/share/ansible && sudo coverage run --data-file=$CI_PROJECT_DIR/.coverage --include=plugins/modules/qubesos.py,plugins/connection/qubes.py -m pytest -vvv tests/qubes/

r4.3:mgmt-host:
extends: .mgmt-host-template
needs:
- r4.3:build:host-fc41
variables:
VM_IMAGE: qubes_4.3_64bit_stable.qcow2
4 changes: 2 additions & 2 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,10 @@ The module supports the following states:
- **pause**
- **running**
- **shutdown**
- **undefine**
- **absent**
- **present**

**Warning:** The `undefine` state will remove the qube and all associated data. Use with caution.
**Warning:** The `absent` state will remove the qube and all associated data. Use with caution.

## Different available commands

Expand Down
52 changes: 52 additions & 0 deletions ci/codecov-keys.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
=ch7z
-----END PGP PUBLIC KEY BLOCK-----
28 changes: 28 additions & 0 deletions ci/codecov-wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash

set -xe

LOCALDIR="$(dirname "$0")"

curl -Os https://uploader.codecov.io/latest/linux/codecov
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig

sqv --keyring "$LOCALDIR"/codecov-keys.asc codecov.SHA256SUM.sig codecov.SHA256SUM
shasum -a 256 -c codecov.SHA256SUM

chmod +x codecov

python3 -m coverage xml || :

if [[ "$CI_COMMIT_BRANCH" =~ ^pr- ]]; then
PR=${CI_COMMIT_BRANCH#pr-}
parents=$(git show -s --format='%P %ae')
if [ $(wc -w <<<"$parents") -eq 3 ] && [ "${parents##* }" = "[email protected]" ]; then
commit_sha=$(cut -f 2 -d ' ' <<<"${parents}")
else
commit_sha=$(git show -s --format='%H')
fi
exec ./codecov --pr "$PR" --sha "$commit_sha" "$@"
fi
exec ./codecov "$@"
2 changes: 2 additions & 0 deletions ci/coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
source = plugins
46 changes: 22 additions & 24 deletions plugins/modules/qubesos.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
- When set to C(shutdown), ensures the VM is stopped.
- When set to C(destroyed), forces the VM to shut down.
- When set to C(pause), pauses a running VM.
- When set to C(undefine), removes the VM definition.
choices: [ present, running, shutdown, destroyed, pause, undefine ]
- When set to C(absent), removes the VM definition.
choices: [ present, running, shutdown, destroyed, pause, absent ]
command:
description:
- Non-idempotent command to execute on the VM.
Expand Down Expand Up @@ -147,6 +147,7 @@
"destroy",
"pause",
"shutdown",
"remove",
"status",
"start",
"stop",
Expand Down Expand Up @@ -333,11 +334,11 @@ def destroy(self, vmname):
"""Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down."""

vm = self.get_vm(vmname)
vm.force_shutdown()
vm.kill()
return 0

def properties(self, vmname, prefs, vmtype, label, vmtemplate):
"Sets the given properties to the VM"
"""Sets the given properties to the VM"""
changed = False
values_changed = []
try:
Expand Down Expand Up @@ -455,14 +456,13 @@ def properties(self, vmname, prefs, vmtype, label, vmtemplate):

return changed, values_changed

def undefine(self, vmname):
"""Stop a domain, and then wipe it from the face of the earth. (delete disk/config file)"""
def remove(self, vmname):
"""Stop a domain, and then wipe it from the face of the earth. (delete disk/config file)"""
try:
self.destroy(vmname)
except QubesVMNotStartedError:
pass
# Because it is not running

pass
while True:
if self.__get_state(vmname) == "shutdown":
break
Expand All @@ -477,7 +477,7 @@ def status(self, vmname):
return self.__get_state(vmname)

def tags(self, vmname, tags):
"Adds a list of tags to the vm"
"""Adds a list of tags to the vm"""
vm = self.get_vm(vmname)
for tag in tags:
vm.tags.add(tag)
Expand Down Expand Up @@ -615,7 +615,6 @@ def core(module):
if not isinstance(res, dict):
res = {command: res}
return VIRT_SUCCESS, res

elif hasattr(v, command):
res = getattr(v, command)()
if not isinstance(res, dict):
Expand All @@ -627,37 +626,36 @@ def core(module):

if state:
if not guest:
module.fail_json(msg="state change requires a guest specified")

module.fail_json(msg="State change requires a guest specified")
if state == "running":
if v.status(guest) is "paused":
if v.status(guest) == "paused":
res["changed"] = True
res["msg"] = v.unpause(guest)
elif v.status(guest) is not "running":
elif v.status(guest) != "running":
res["changed"] = True
res["msg"] = v.start(guest)
elif state == "shutdown":
if v.status(guest) is not "shutdown":
if v.status(guest) != "shutdown":
res["changed"] = True
res["msg"] = v.shutdown(guest)
elif state == "destroyed":
if v.status(guest) is not "shutdown":
if v.status(guest) != "shutdown":
res["changed"] = True
res["msg"] = v.destroy(guest)
elif state == "paused":
if v.status(guest) is "running":
if v.status(guest) == "running":
res["changed"] = True
res["msg"] = v.pause(guest)
elif state == "undefine":
if v.status(guest) is not "shutdown":
elif state == "absent":
if v.status(guest) == "shutdown":
res["changed"] = True
res["msg"] = v.undefine(guest)
res["msg"] = v.remove(guest)
else:
module.fail_json(msg="unexpected state")
module.fail_json(msg="Unexpected state")

return VIRT_SUCCESS, res

module.fail_json(msg="expected state or command parameter to be specified")
module.fail_json(msg="Expected state or command parameter to be specified")


def main():
Expand All @@ -671,14 +669,14 @@ def main():
"pause",
"running",
"shutdown",
"undefine",
"absent",
"present",
],
),
command=dict(type="str", choices=ALL_COMMANDS),
label=dict(type="str", default="red"),
vmtype=dict(type="str", default="AppVM"),
template=dict(type="str", default="default"),
template=dict(type="str", default=None),
properties=dict(type="dict", default={}),
tags=dict(type="list", default=[]),
),
Expand Down
17 changes: 16 additions & 1 deletion qubes-ansible.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@ BuildArch: noarch
Source0: %{name}-%{version}.tar.gz

Requires: ansible
Requires: qubes-core-admin-client

%description
qubes-ansible provides an Ansible connection plugin ("qubes") and an Ansible module ("qubesos")
to manage QubesOS virtual machines. The files are installed into the Ansible module directory so that
Ansible can automatically discover and use them. This package is intended to be installed in dom0
(or a management qube).

%package tests
Summary: Tests for the module and the connection
Requires: %{name}
Requires: python3-pytest

%description tests
Tests for the module and the connection.

%prep
%autosetup

Expand All @@ -25,15 +34,21 @@ Ansible can automatically discover and use them. This package is intended to be
rm -rf %{buildroot}
%{__mkdir} -p %{buildroot}%{_datadir}/ansible/plugins/modules
%{__mkdir} -p %{buildroot}%{_datadir}/ansible/plugins/connection
%{__mkdir} -p %{buildroot}%{_datadir}/ansible/tests/qubes

# Install the qubesos module and qubes connection plugin
install -m 644 plugins/modules/qubesos.py %{buildroot}%{_datadir}/ansible/plugins/modules/qubesos.py
install -m 644 plugins/connection/qubes.py %{buildroot}%{_datadir}/ansible/plugins/connection/qubes.py

install -m 644 tests/*.py %{buildroot}%{_datadir}/ansible/tests/qubes/

%files
%doc README.md LICENSE EXAMPLES.md
%{_datadir}/ansible/plugins/modules/qubesos.py
%{_datadir}/ansible/plugins/connection/qubes.py

%files tests
%{_datadir}/ansible/tests/qubes/*.py

%changelog
@CHANGELOG@
@CHANGELOG@
51 changes: 51 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import uuid

import pytest
import qubesadmin

from plugins.modules.qubesos import core


# Helper to run the module core function
class Module:
def __init__(self, params):
self.params = params

def fail_json(self, **kwargs):
pytest.fail(f"Module failed: {kwargs}")

def exit_json(self, **kwargs):
pass


@pytest.fixture(scope="function")
def qubes():
"""Return a Qubes app instance"""
try:
return qubesadmin.Qubes()
except Exception as e:
pytest.skip(f"Qubes API not available: {e}")


@pytest.fixture(scope="function")
def vmname():
"""Generate a random VM name for testing"""
return f"test-vm-{uuid.uuid4().hex[:8]}"


@pytest.fixture(autouse=True)
def cleanup_vm(qubes, request):
"""Ensure any test VM is removed after test"""
created = []

def mark(name):
created.append(name)

request.node.mark_vm_created = mark
yield
# Teardown (remove VMs)
for name in created:
try:
core(Module({"command": "remove", "name": name}))
except Exception:
pass
Loading