From c1f651d972dbb6a987918d68b560dff5809e0ac8 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Mon, 15 Jul 2024 10:35:34 +0200 Subject: [PATCH] feat(kubevirt_vm): Add support for RunStrategy This change adds support for setting the RunStrategy of a VM. Depending on the value set the wait condition for the VM is adjusted. For the values Always, RerunOnFailure or Once the wait condition will wait for the VM to run and be ready. For the value Halted the wait condition will wait for the VM to not exist. For the value Manual the wait condition is not set. Signed-off-by: Felix Matouschek --- examples/play-create-run-strategy.yml | 42 +++++++ plugins/modules/kubevirt_vm.py | 59 +++++++-- .../unit/plugins/modules/test_kubevirt_vm.py | 112 +++++++++++++++++- 3 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 examples/play-create-run-strategy.yml diff --git a/examples/play-create-run-strategy.yml b/examples/play-create-run-strategy.yml new file mode 100644 index 0000000..837b89e --- /dev/null +++ b/examples/play-create-run-strategy.yml @@ -0,0 +1,42 @@ +--- +- name: Playbook creating a virtual machine with multus network + hosts: localhost + tasks: + - name: Create VM + kubevirt.core.kubevirt_vm: + state: present + name: testvm + namespace: default + labels: + app: test + instancetype: + name: u1.medium + preference: + name: fedora + run_strategy: Manual + spec: + domain: + devices: + interfaces: + - name: default + masquerade: {} + - name: bridge-network + bridge: {} + networks: + - name: default + pod: {} + - name: bridge-network + multus: + networkName: kindexgw + volumes: + - containerDisk: + image: quay.io/containerdisks/fedora:latest + name: containerdisk + - cloudInitNoCloud: + userData: |- + #cloud-config + # The default username is: fedora + ssh_authorized_keys: + - ssh-ed25519 AAAA... + name: cloudinit + wait: true diff --git a/plugins/modules/kubevirt_vm.py b/plugins/modules/kubevirt_vm.py index 9199f55..112993a 100644 --- a/plugins/modules/kubevirt_vm.py +++ b/plugins/modules/kubevirt_vm.py @@ -61,8 +61,21 @@ options: running: description: - Specify whether the C(VirtualMachine) should be running or not. + - Mutually exclusive with O(run_strategy). + - Defaults to O(running=yes) when O(running) and O(run_strategy) are not set. type: bool - default: yes + run_strategy: + description: + - Specify the C(RunStrategy) of the C(VirtualMachine). + - Mutually exclusive with O(running). + type: str + choices: + - Always + - Halted + - Manual + - RerunOnFailure + - Once + version_added: 2.0.0 instancetype: description: - Specify the C(Instancetype) matcher of the C(VirtualMachine). @@ -280,6 +293,13 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions imp CoreException, ) +WAIT_CONDITION_READY = {"type": "Ready", "status": True} +WAIT_CONDITION_VMI_NOT_EXISTS = { + "type": "Ready", + "status": False, + "reason": "VMINotExists", +} + def create_vm(params: Dict) -> Dict: """ @@ -292,7 +312,6 @@ def create_vm(params: Dict) -> Dict: "namespace": params["namespace"], }, "spec": { - "running": params["running"], "template": {"spec": {"domain": {"devices": {}}}}, }, } @@ -312,6 +331,12 @@ def create_vm(params: Dict) -> Dict: if template_metadata: vm["spec"]["template"]["metadata"] = template_metadata + if (run_strategy := params.get("run_strategy")) is not None: + vm["spec"]["runStrategy"] = run_strategy + else: + vm["spec"]["running"] = ( + running if (running := params.get("running")) is not None else True + ) if (instancetype := params.get("instancetype")) is not None: vm["spec"]["instancetype"] = instancetype if (preference := params.get("preference")) is not None: @@ -324,6 +349,21 @@ def create_vm(params: Dict) -> Dict: return vm +def set_wait_condition(module: AnsibleK8SModule) -> None: + """ + set_wait_condition sets the wait_condition to allow waiting for the ready + state of the VirtualMachine depending on the module parameters running + and run_strategy. + """ + if ( + module.params["running"] is False + or (run_strategy := module.params["run_strategy"]) == "Halted" + ): + module.params["wait_condition"] = WAIT_CONDITION_VMI_NOT_EXISTS + elif run_strategy != "Manual": + module.params["wait_condition"] = WAIT_CONDITION_READY + + def arg_spec() -> Dict: """ arg_spec defines the argument spec of this module. @@ -335,7 +375,10 @@ def arg_spec() -> Dict: "namespace": {"required": True}, "annotations": {"type": "dict"}, "labels": {"type": "dict"}, - "running": {"type": "bool", "default": True}, + "running": {"type": "bool"}, + "run_strategy": { + "choices": ["Always", "Halted", "Manual", "RerunOnFailure", "Once"] + }, "instancetype": {"type": "dict"}, "preference": {"type": "dict"}, "data_volume_templates": {"type": "list", "elements": "dict"}, @@ -376,6 +419,7 @@ def main() -> None: argument_spec=arg_spec(), mutually_exclusive=[ ("name", "generate_name"), + ("running", "run_strategy"), ], required_one_of=[ ("name", "generate_name"), @@ -387,14 +431,7 @@ def main() -> None: module.params["resource_definition"] = create_vm(module.params) # Set wait_condition to allow waiting for the ready state of the VirtualMachine - if module.params["running"]: - module.params["wait_condition"] = {"type": "Ready", "status": True} - else: - module.params["wait_condition"] = { - "type": "Ready", - "status": False, - "reason": "VMINotExists", - } + set_wait_condition(module) try: runner.run_module(module) diff --git a/tests/unit/plugins/modules/test_kubevirt_vm.py b/tests/unit/plugins/modules/test_kubevirt_vm.py index 436904b..15d36ac 100644 --- a/tests/unit/plugins/modules/test_kubevirt_vm.py +++ b/tests/unit/plugins/modules/test_kubevirt_vm.py @@ -101,11 +101,27 @@ VM_DEFINITION_STOPPED = { }, } +VM_DEFINITION_HALTED = { + "apiVersion": "kubevirt.io/v1", + "kind": "VirtualMachine", + "metadata": { + "name": "testvm", + "namespace": "default", + }, + "spec": { + "runStrategy": "Halted", + "template": { + "spec": { + "domain": {"devices": {}}, + }, + }, + }, +} + MODULE_PARAMS_DEFAULT = { "api_version": "kubevirt.io/v1", "annotations": None, "labels": None, - "running": True, "instancetype": None, "preference": None, "data_volume_templates": None, @@ -174,6 +190,12 @@ MODULE_PARAMS_STOPPED = MODULE_PARAMS_DEFAULT | { "running": False, } +MODULE_PARAMS_HALTED = MODULE_PARAMS_DEFAULT | { + "name": "testvm", + "namespace": "default", + "run_strategy": "Halted", +} + MODULE_PARAMS_DELETE = MODULE_PARAMS_DEFAULT | { "name": "testvm", "namespace": "default", @@ -183,24 +205,37 @@ MODULE_PARAMS_DELETE = MODULE_PARAMS_DEFAULT | { K8S_MODULE_PARAMS_CREATE = MODULE_PARAMS_CREATE | { "generate_name": None, + "running": None, + "run_strategy": None, "resource_definition": VM_DEFINITION_CREATE, "wait_condition": {"type": "Ready", "status": True}, } K8S_MODULE_PARAMS_RUNNING = MODULE_PARAMS_RUNNING | { "generate_name": None, + "run_strategy": None, "resource_definition": VM_DEFINITION_RUNNING, "wait_condition": {"type": "Ready", "status": True}, } K8S_MODULE_PARAMS_STOPPED = MODULE_PARAMS_STOPPED | { "generate_name": None, + "run_strategy": None, "resource_definition": VM_DEFINITION_STOPPED, "wait_condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, } +K8S_MODULE_PARAMS_HALTED = MODULE_PARAMS_HALTED | { + "generate_name": None, + "running": None, + "resource_definition": VM_DEFINITION_HALTED, + "wait_condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, +} + K8S_MODULE_PARAMS_DELETE = MODULE_PARAMS_DELETE | { "generate_name": None, + "running": None, + "run_strategy": None, "resource_definition": VM_DEFINITION_RUNNING, "wait_condition": {"type": "Ready", "status": True}, } @@ -227,6 +262,12 @@ K8S_MODULE_PARAMS_DELETE = MODULE_PARAMS_DELETE | { VM_DEFINITION_STOPPED, "update", ), + ( + MODULE_PARAMS_HALTED, + K8S_MODULE_PARAMS_HALTED, + VM_DEFINITION_HALTED, + "update", + ), ( MODULE_PARAMS_DELETE, K8S_MODULE_PARAMS_DELETE, @@ -262,10 +303,15 @@ def test_module(mocker, module_params, k8s_module_params, vm_definition, method) CREATE_VM_PARAMS = { "api_version": "kubevirt.io/v1", - "running": True, "namespace": "default", } +CREATE_VM_PARAMS_RUN_STRATEGY = { + "api_version": "kubevirt.io/v1", + "namespace": "default", + "run_strategy": "Manual", +} + CREATE_VM_PARAMS_ANNOTATIONS = CREATE_VM_PARAMS | { "annotations": {"test": "test"}, } @@ -341,6 +387,24 @@ CREATED_VM = { }, } +CREATED_VM_RUN_STRATEGY = { + "apiVersion": "kubevirt.io/v1", + "kind": "VirtualMachine", + "metadata": { + "namespace": "default", + }, + "spec": { + "runStrategy": "Manual", + "template": { + "spec": { + "domain": { + "devices": {}, + }, + }, + }, + }, +} + CREATED_VM_LABELS = { "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachine", @@ -528,6 +592,7 @@ CREATED_VM_SPECS = { "params,expected", [ (CREATE_VM_PARAMS, CREATED_VM), + (CREATE_VM_PARAMS_RUN_STRATEGY, CREATED_VM_RUN_STRATEGY), (CREATE_VM_PARAMS_ANNOTATIONS, CREATED_VM_ANNOTATIONS), (CREATE_VM_PARAMS_LABELS, CREATED_VM_LABELS), (CREATE_VM_PARAMS_INSTANCETYPE, CREATED_VM_INSTANCETYPE), @@ -540,3 +605,46 @@ CREATED_VM_SPECS = { ) def test_create_vm(params, expected): assert kubevirt_vm.create_vm(params) == expected + + +@pytest.mark.parametrize( + "params,expected", + [ + ({"running": None, "run_strategy": "Manual"}, {}), + ( + {"running": None, "run_strategy": None}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_READY}, + ), + ( + {"running": True, "run_strategy": None}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_READY}, + ), + ( + {"running": None, "run_strategy": "Always"}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_READY}, + ), + ( + {"running": None, "run_strategy": "RerunOnFailure"}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_READY}, + ), + ( + {"running": None, "run_strategy": "Once"}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_READY}, + ), + ( + {"running": False, "run_strategy": None}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_VMI_NOT_EXISTS}, + ), + ( + {"running": None, "run_strategy": "Halted"}, + {"wait_condition": kubevirt_vm.WAIT_CONDITION_VMI_NOT_EXISTS}, + ), + ], +) +def test_set_wait_condition(mocker, params, expected): + module = mocker.Mock() + module.params = params + + kubevirt_vm.set_wait_condition(module) + + assert module.params == params | expected