Merge pull request #242 from jcanocan/start-stop-restart-vms

feat(kubevirt_vm): add `state=started/stopped/restarted` to manage VM lifecycle
This commit is contained in:
kubevirt-bot
2026-06-08 17:18:06 +02:00
committed by GitHub
6 changed files with 840 additions and 13 deletions

View File

@@ -11,13 +11,13 @@ __metaclass__ = type
DOCUMENTATION = """
module: kubevirt_vm
short_description: Create or delete KubeVirt VirtualMachines
short_description: Manage KubeVirt VirtualMachines
author:
- "KubeVirt.io Project (!UNKNOWN)"
description:
- Use the Kubernetes Python client to perform create or delete operations on KubeVirt VirtualMachines.
- Use the Kubernetes Python client to perform create, delete, start, stop, or restart operations on KubeVirt VirtualMachines.
- Pass options to create the VirtualMachine as module arguments.
- Authenticate using either a config file, certificates, password or token.
- Supports check mode.
@@ -76,6 +76,13 @@ options:
- RerunOnFailure
- Once
version_added: 2.0.0
grace_period_seconds:
description:
- Specify the grace period in seconds for stopping or restarting the C(VirtualMachine).
- Only used when O(state=stopped) or O(state=restarted).
- When used with O(state=restarted), the only supported values are V(null) (use the default grace period) and V(0) (force restart immediately).
type: int
version_added: 2.3.0
instancetype:
description:
- Specify the C(Instancetype) matcher of the C(VirtualMachine).
@@ -144,15 +151,21 @@ options:
type: str
state:
description:
- Determines if an object should be created, patched, or deleted.
- Determines if an object should be created, patched, deleted, started, stopped, or restarted.
- When set to O(state=present), an object will be created, if it does not already exist.
- If set to O(state=absent), an existing object will be deleted.
- If set to O(state=present), an existing object will be patched, if its attributes differ from those specified.
- If set to O(state=started), it will be ensured that the C(VirtualMachine) is running. Added in version 2.3.0.
- If set to O(state=stopped), it will be ensured that the C(VirtualMachine) is stopped. Added in version 2.3.0.
- If set to O(state=restarted), it will be ensured that the C(VirtualMachine) is restarted. Added in version 2.3.0.
type: str
default: present
choices:
- absent
- present
- restarted
- started
- stopped
force:
description:
- If set to O(force=yes), and O(state=present) is set, an existing object will be replaced.
@@ -258,6 +271,35 @@ EXAMPLES = """
name: testvm
namespace: default
state: absent
- name: Start a VirtualMachine
kubevirt.core.kubevirt_vm:
name: testvm
namespace: default
state: started
wait: true
- name: Stop a VirtualMachine
kubevirt.core.kubevirt_vm:
name: testvm
namespace: default
state: stopped
wait: true
- name: Restart a VirtualMachine
kubevirt.core.kubevirt_vm:
name: testvm
namespace: default
state: restarted
wait: true
- name: Force restart a VirtualMachine
kubevirt.core.kubevirt_vm:
name: testvm
namespace: default
state: restarted
grace_period_seconds: 0
wait: true
"""
RETURN = """
@@ -300,12 +342,24 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import
from ansible_collections.kubernetes.core.plugins.module_utils.k8s import (
runner,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
get_api_client,
K8SClient,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
AnsibleK8SModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
CoreException,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
K8sService,
)
try:
from kubernetes.client.exceptions import ApiException
except ImportError:
pass
WAIT_CONDITION_READY = {"type": "Ready", "status": True}
WAIT_CONDITION_VMI_NOT_EXISTS = {
@@ -313,6 +367,8 @@ WAIT_CONDITION_VMI_NOT_EXISTS = {
"status": False,
"reason": "VMINotExists",
}
SUBRESOURCE_API = "subresources.kubevirt.io/v1"
VM_IS_ALREADY_IN_DESIRED_STATE_ERROR_CODE = 409
def create_vm(params: Dict) -> Dict:
@@ -380,6 +436,133 @@ def set_wait_condition(module: AnsibleK8SModule) -> None:
module.params["wait_condition"] = WAIT_CONDITION_READY
def _apply_definition(module: AnsibleK8SModule) -> Dict:
definition = create_vm(module.params)
original_state = module.params["state"]
original_wait = module.params.get("wait")
if original_state not in ("present", "absent"):
module.params["state"] = "present"
# Don't wait during the CRUD step for subresource states;
# the subresource action handles the final wait.
module.params["wait"] = False
# Don't override running/runStrategy unless the user explicitly set
# them, so the CRUD step won't unintentionally change the VM's
# lifecycle state before the subresource action runs.
if (
module.params.get("running") is None
and module.params.get("run_strategy") is None
):
definition["spec"].pop("running", None)
definition["spec"].pop("runStrategy", None)
module.params["resource_definition"] = definition
set_wait_condition(module)
captured = {}
original_exit_json = module.exit_json
module.exit_json = lambda **kwargs: captured.update(kwargs)
try:
runner.run_module(module)
except CoreException as exc:
module.fail_from_exception(exc)
finally:
module.exit_json = original_exit_json
module.params["state"] = original_state
module.params["wait"] = original_wait
return captured
def _call_subresource(
client: K8SClient,
module: AnsibleK8SModule,
action: str,
body: Dict,
wait_condition: Dict,
ignore_conflict: bool = False,
) -> Dict:
name = module.params["name"]
namespace = module.params["namespace"]
path = (
f"/apis/{SUBRESOURCE_API}"
f"/namespaces/{namespace}"
f"/virtualmachines/{name}/{action}"
)
try:
client.client.request("put", path, body=body, header_params={"Accept": "*/*"})
except ApiException as exc:
# 409 Conflict means the VM is already in the desired state or
# its RunStrategy does not support manual start requests.
if ignore_conflict and exc.status == VM_IS_ALREADY_IN_DESIRED_STATE_ERROR_CODE:
return {"changed": False}
module.fail_json(
msg=f"Failed to {action} VirtualMachine '{name}': {exc}",
)
result = {"changed": True}
if module.params.get("wait"):
try:
svc = K8sService(client, module)
wait_result = svc.find(
kind="VirtualMachine",
api_version=module.params["api_version"],
name=name,
namespace=namespace,
wait=True,
wait_sleep=module.params["wait_sleep"],
wait_timeout=module.params["wait_timeout"],
condition=wait_condition,
)
result.update(wait_result)
except CoreException as exc:
module.fail_from_exception(exc)
return result
def start_vm(client: K8SClient, module: AnsibleK8SModule) -> Dict:
if module.check_mode:
return {"changed": True}
return _call_subresource(
client, module, "start", None, WAIT_CONDITION_READY, ignore_conflict=True
)
def stop_vm(client: K8SClient, module: AnsibleK8SModule) -> Dict:
if module.check_mode:
return {"changed": True}
grace_period_seconds = module.params.get("grace_period_seconds")
body = None
if grace_period_seconds is not None:
body = {"gracePeriod": grace_period_seconds}
return _call_subresource(
client,
module,
"stop",
body,
WAIT_CONDITION_VMI_NOT_EXISTS,
ignore_conflict=True,
)
def restart_vm(client: K8SClient, module: AnsibleK8SModule) -> Dict:
if module.check_mode:
return {"changed": True}
grace_period_seconds = module.params.get("grace_period_seconds")
body = None
if grace_period_seconds is not None:
body = {"gracePeriodSeconds": grace_period_seconds}
return _call_subresource(client, module, "restart", body, WAIT_CONDITION_READY)
def arg_spec() -> Dict:
"""
arg_spec defines the argument spec of this module.
@@ -395,6 +578,7 @@ def arg_spec() -> Dict:
"run_strategy": {
"choices": ["Always", "Halted", "Manual", "RerunOnFailure", "Once"]
},
"grace_period_seconds": {"type": "int"},
"instancetype": {"type": "dict"},
"preference": {"type": "dict"},
"data_volume_templates": {"type": "list", "elements": "dict"},
@@ -430,6 +614,13 @@ def arg_spec() -> Dict:
spec.update(deepcopy(AUTH_ARG_SPEC))
spec.update(deepcopy(COMMON_ARG_SPEC))
# Override state choices from COMMON_ARG_SPEC which only defines
# ["present", "absent"], adding our subresource action states.
spec["state"] = {
"default": "present",
"choices": ["absent", "present", "restarted", "started", "stopped"],
}
return spec
@@ -448,19 +639,27 @@ def main() -> None:
required_one_of=[
("name", "generate_name"),
],
required_if=[
("state", "started", ("name",)),
("state", "stopped", ("name",)),
("state", "restarted", ("name",)),
],
supports_check_mode=True,
)
# Set resource_definition to our constructed VM
module.params["resource_definition"] = create_vm(module.params)
result = _apply_definition(module)
# Set wait_condition to allow waiting for the ready state of the VirtualMachine
set_wait_condition(module)
subresource_actions = {
"started": start_vm,
"stopped": stop_vm,
"restarted": restart_vm,
}
if (action := subresource_actions.get(module.params["state"])) is not None:
client = get_api_client(module)
action_result = action(client, module)
result["changed"] = result["changed"] or action_result.get("changed", False)
try:
runner.run_module(module)
except CoreException as exc:
module.fail_from_exception(exc)
module.exit_json(**result)
if __name__ == "__main__":