mirror of
https://github.com/kubevirt/kubevirt.core.git
synced 2026-07-25 00:44:35 +00:00
feat(kubevirt_vm): add state=start/stop/restart to manage VM lifecycle
It adds support for starting, stopping, and restarting VirtualMachines. The operations are performed through the KubeVirt restart subresource API (PUT to `subresources.kubevirt.io/v1`). These calls are extracted into a single shared helper function `_call_subresource()` to handle the common flow, i.e., check mode, API call and wait conditions. Moreover, it adds the optional parameter `grace_period_seconds` for stop and start operations. Additionally, it adds unit tests and integration tests for all operations. Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Javier Cano Cano <jcanocan@redhat.com>
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user