Skip to content

Commit 02a1553

Browse files
committed
vm_params need to start/shutdown/reboot etc VM if requested
Signed-off-by: Justin Cinkelj <justin.cinkelj@xlab.si>
1 parent 2794a34 commit 02a1553

File tree

6 files changed

+124
-35
lines changed

6 files changed

+124
-35
lines changed

plugins/module_utils/vm.py

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ def __init__(
295295
# self._did_start_work = False
296296
# Now, VM was rebooted when it was running at start, then stopped and powered on:
297297
# .power_state in [] and any(_did_nice_shutdown_work, _did_force_shutdown_work) and _was_start_tried
298+
self._was_reboot_tried = False
299+
self._was_reset_tried = False
298300

299301
@property
300302
def nic_list(self):
@@ -853,40 +855,87 @@ def get_vm_and_boot_devices(cls, ansible_dict, rest_client):
853855
]
854856
return vm, boot_devices_uuid, boot_devices_ansible
855857

856-
def update_vm_power_state(self, module, rest_client, desired_power_action):
858+
def update_vm_power_state(
859+
self, module, rest_client, desired_power_action, ignore_repeated_request: bool
860+
):
857861
"""Sets the power state to what is stored in self.power_state"""
862+
858863
# desired_power_action must be present in FROM_ANSIBLE_TO_HYPERCORE_POWER_ACTION's keys
864+
def assert_or_ignore_repeated_request(msg):
865+
if ignore_repeated_request:
866+
# Do not start/stop/shutdown VM twice.
867+
# Normally we want to assert if a second start/stop/shutdown is tried.
868+
# But as very last module change it makes sense to push VM into requested power_state,
869+
# without knowing what did module already do with VM power_state.
870+
# So in this special case, allow a second call, and just silently ignore
871+
# it if VM power_state was already set to desired state.
872+
return
873+
else:
874+
raise AssertionError(msg)
875+
859876
if not self._power_state:
860877
raise errors.ScaleComputingError("No information about VM's power state.")
861878

862879
# keep a record what was done
863880
if desired_power_action == "start":
864881
if self._was_start_tried:
865-
raise AssertionError("VM _was_start_tried already set")
882+
return assert_or_ignore_repeated_request(
883+
"VM _was_start_tried already set"
884+
)
866885
self._was_start_tried = True
867886
if desired_power_action == "shutdown":
868887
if self._was_nice_shutdown_tried:
869-
raise AssertionError("VM _was_nice_shutdown_tried already set")
888+
return assert_or_ignore_repeated_request(
889+
"VM _was_nice_shutdown_tried already set"
890+
)
870891
self._was_nice_shutdown_tried = True
871892
if desired_power_action == "stop":
872893
if self._was_force_shutdown_tried:
873-
raise AssertionError("VM _was_force_shutdown_tried already set")
894+
return assert_or_ignore_repeated_request(
895+
"VM _was_force_shutdown_tried already set"
896+
)
874897
self._was_force_shutdown_tried = True
875-
# reboot, reset are not included
876-
877-
task_tag = rest_client.create_record(
878-
"/rest/v1/VirDomain/action",
879-
[
880-
dict(
881-
virDomainUUID=self.uuid,
882-
actionType=FROM_ANSIBLE_TO_HYPERCORE_POWER_ACTION[
883-
desired_power_action
884-
],
885-
cause="INTERNAL",
898+
if desired_power_action == "reboot":
899+
if self._was_reboot_tried:
900+
return assert_or_ignore_repeated_request(
901+
"VM _was_reboot_tried already set"
886902
)
887-
],
888-
module.check_mode,
889-
)
903+
self._was_reboot_tried = True
904+
if desired_power_action == "reset":
905+
if self._was_reset_tried:
906+
return assert_or_ignore_repeated_request(
907+
"VM _was_reset_tried already set"
908+
)
909+
self._was_reset_tried = True
910+
911+
try:
912+
task_tag = rest_client.create_record(
913+
"/rest/v1/VirDomain/action",
914+
[
915+
dict(
916+
virDomainUUID=self.uuid,
917+
actionType=FROM_ANSIBLE_TO_HYPERCORE_POWER_ACTION[
918+
desired_power_action
919+
],
920+
cause="INTERNAL",
921+
)
922+
],
923+
module.check_mode,
924+
)
925+
except errors.UnexpectedAPIResponse as ex:
926+
if desired_power_action != "reset":
927+
raise
928+
# We are allowed to send reset only if VM is in
929+
# RUNNING or SHUTDOWN (as in middle of shutting down, but not yet fully shutdown).
930+
# If VM is already shutoff, the request fails.
931+
# Ignore this special case.
932+
# The whole RESET is not even exposed on HyperCore UI,
933+
# maybe we should remove it from ansible.
934+
if ex.response_status != 500:
935+
# the '500 b'{"error":"An internal error occurred"}'' is the one to ignore
936+
raise
937+
module.warn("Ignoring failed VM RESET")
938+
return
890939
TaskTag.wait_task(rest_client, task_tag)
891940

892941
@classmethod
@@ -936,7 +985,7 @@ def vm_shutdown_forced(self, module, rest_client, reboot=False):
936985
if vm_fresh_data["state"] in ["SHUTOFF", "SHUTDOWN"]:
937986
return True
938987
if module.params["force_reboot"] and self._was_nice_shutdown_tried:
939-
self.update_vm_power_state(module, rest_client, "stop")
988+
self.update_vm_power_state(module, rest_client, "stop", False)
940989
# force shutdown should always work. If not, we need to pool for state change.
941990
# Maybe we need to pool for state change anyway -
942991
# TaskTag might be finished before VM is really off.
@@ -959,7 +1008,7 @@ def wait_shutdown(self, module, rest_client):
9591008
# and module.params["shutdown_timeout"] # default is 300 anyway
9601009
and not self._was_nice_shutdown_tried
9611010
):
962-
self.update_vm_power_state(module, rest_client, "shutdown")
1011+
self.update_vm_power_state(module, rest_client, "shutdown", False)
9631012
shutdown_timeout = module.params["shutdown_timeout"]
9641013
start = time()
9651014
while 1:
@@ -983,14 +1032,14 @@ def vm_power_up(self, module, rest_client):
9831032
# - VM was initially stopped and
9841033
# - module param power_state is omitted or contains "stop".
9851034
if self.was_vm_shutdown() and self._initially_running:
986-
self.update_vm_power_state(module, rest_client, "start")
1035+
self.update_vm_power_state(module, rest_client, "start", False)
9871036
return
9881037
# Also start VM if module power_state requires a power on.
9891038
# Field _power_action is set only if VM instance was created with from_ansible();
9901039
# it is None if VM instance was created with from_hypercore().
9911040
requested_power_action = module.params.get("power_state")
9921041
if requested_power_action == "start":
993-
self.update_vm_power_state(module, rest_client, "start")
1042+
self.update_vm_power_state(module, rest_client, "start", False)
9941043

9951044
def was_vm_shutdown(self) -> bool:
9961045
"""
@@ -1236,21 +1285,26 @@ def set_vm_params(cls, module, rest_client, vm, param_subset: List[str]):
12361285
endpoint = "{0}/{1}".format("/rest/v1/VirDomain", vm.uuid)
12371286
task_tag = rest_client.update_record(endpoint, payload, module.check_mode)
12381287
TaskTag.wait_task(rest_client, task_tag)
1288+
1289+
# shutdown VM if it needs to be rebooted to apply NIC/disk changes
12391290
if ManageVMParams._needs_reboot(
12401291
module, changed_parameters
12411292
) and vm._power_action not in ["stop", "stopped", "shutdown"]:
12421293
vm.do_shutdown_steps(module, rest_client)
1294+
12431295
return (
12441296
True,
12451297
dict(
12461298
before=ManageVMParams._build_before_diff(vm, module),
12471299
after=ManageVMParams._build_after_diff(module, rest_client),
12481300
),
1301+
changed_parameters,
12491302
)
12501303
else:
12511304
return (
12521305
False,
12531306
dict(before=None, after=None),
1307+
changed_parameters,
12541308
)
12551309

12561310
@classmethod

plugins/modules/vm.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -444,10 +444,10 @@ def _set_nics(module, rest_client, vm_before: VM):
444444

445445

446446
def _set_vm_params(module, rest_client, vm, param_subset: List[str]):
447-
changed_params, diff = ManageVMParams.set_vm_params(
447+
changed_params, diff, changed_parameters = ManageVMParams.set_vm_params(
448448
module, rest_client, vm, param_subset
449449
)
450-
return changed_params
450+
return changed_params, changed_parameters
451451

452452

453453
def ensure_present(module, rest_client):
@@ -457,7 +457,7 @@ def ensure_present(module, rest_client):
457457
existing_boot_order = vm_before.get_boot_device_order()
458458
# machineType needs to be set to uefi/vtpm first,
459459
# next nvram/vtpm disk can be added.
460-
changed_params_1 = _set_vm_params(
460+
changed_params_1, changed_parameters_1 = _set_vm_params(
461461
module, rest_client, vm_before, param_subset=["machine_type"]
462462
)
463463
changed_disks = _set_disks(module, rest_client, vm_before)
@@ -470,7 +470,7 @@ def ensure_present(module, rest_client):
470470
# since boot order cannot be set when the vm is running.
471471
# set_vm_params updates VM's name, description, tags, memory, number of CPU,
472472
# changed the power state and/or assigns the snapshot schedule to the VM
473-
changed_params_2 = _set_vm_params(
473+
changed_params_2, changed_parameters_2 = _set_vm_params(
474474
module, rest_client, vm_before, param_subset=[]
475475
)
476476
changed = any(
@@ -505,7 +505,7 @@ def ensure_present(module, rest_client):
505505
# Set power state
506506
if module.params["power_state"] != "shutdown":
507507
vm_created.update_vm_power_state(
508-
module, rest_client, module.params["power_state"]
508+
module, rest_client, module.params["power_state"], False
509509
)
510510
changed = True
511511
name_field = "vm_name"
@@ -527,7 +527,7 @@ def ensure_absent(module, rest_client):
527527
if vm:
528528
if vm._power_state != "shutdown": # First, shut it off and then delete
529529
# TODO ==shutdown or ==stopped ??
530-
vm.update_vm_power_state(module, rest_client, "stop")
530+
vm.update_vm_power_state(module, rest_client, "stop", False)
531531
task_tag = rest_client.delete_record(
532532
"{0}/{1}".format("/rest/v1/VirDomain", vm.uuid), module.check_mode
533533
)

plugins/modules/vm_params.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,24 @@ def run(module, rest_client):
132132
# Update VM's name, description, tags, memory, number of CPUs, power_state and/or assign snapshot schedule.
133133
# In case if reboot is needed, set_vm_params will shutdown the vm
134134
# In case if reboot is not needed, set_vm_params will set power_state as specified in the module.params["power_state"]
135-
changed, diff = ManageVMParams.set_vm_params(
135+
changed, diff, changed_parameters = ManageVMParams.set_vm_params(
136136
module, rest_client, vm, param_subset=[]
137137
)
138138
if module.params["power_state"] not in ["shutdown", "stop"]:
139139
# VM will be powered on in case if reboot is needed and module.params["power_state"] in ["start", "reboot", "reset"]
140140
# if reboot is not needed, vm_power_up doesn't do anything
141141
vm.vm_power_up(module, rest_client)
142+
143+
# shutdown or start VM if ansible module power_state param require power state change
144+
if changed_parameters.get("power_state"):
145+
requested_power_action = module.params["power_state"]
146+
# vm.vm_power_up can already start VM, and a second start here would assert.
147+
# ManageVMParams.set_vm_params can already shutdown/stop VM.
148+
ignore_repeated_request = True
149+
vm.update_vm_power_state(
150+
module, rest_client, requested_power_action, ignore_repeated_request
151+
)
152+
142153
return changed, vm.was_vm_rebooted(), diff
143154

144155

tests/integration/targets/i_vm_params/tasks/02_machine_type.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,7 @@
210210
- not output.records[0].snapshot_schedule
211211

212212
# ===================================================================
213-
# Set nonexisting snapshot schedule
214-
- name: Set VMs power_state - applied
213+
- name: Set VMs power_state - reset
215214
scale_computing.hypercore.vm_params:
216215
vm_name: "{{ vm_name }}"
217216
power_state: reset
@@ -225,14 +224,18 @@
225224
- output is changed
226225
- output.vm_rebooted is false # true # todo - is this reported? should we pretend reboot==reset - it is not same.
227226

227+
# HC3 returned error for RESET, but it did reset VM (HC3 v9.4.0)
228+
# After RESET, VM was 10-15 sec in stopped state.
229+
- name: Wait on VM stop/start to finish
230+
ansible.builtin.pause:
231+
seconds: 20
228232
- name: Check VMs power_state isn't changed
229233
scale_computing.hypercore.vm_info:
230234
vm_name: "{{ vm_name }}"
231235
register: output
232236
- ansible.builtin.assert:
233237
that:
234238
- output is succeeded
235-
# HC3 9.2.22 - RESET is accepted, but VM remains stopped as it was stopped before.
236239
- output.records[0].power_state == "started"
237240

238241
# ===================================================================

tests/unit/plugins/module_utils/test_vm.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,7 +2060,9 @@ def test_set_vm_params(self, create_module, rest_client, mocker):
20602060
mocker.patch(
20612061
"ansible_collections.scale_computing.hypercore.plugins.module_utils.vm.VM.wait_shutdown"
20622062
).return_value = True
2063-
changed, diff = ManageVMParams.set_vm_params(module, rest_client, vm_before, [])
2063+
changed, diff, changed_parameters = ManageVMParams.set_vm_params(
2064+
module, rest_client, vm_before, []
2065+
)
20642066

20652067
assert changed is True
20662068
assert diff == {
@@ -2083,6 +2085,15 @@ def test_set_vm_params(self, create_module, rest_client, mocker):
20832085
"snapshot_schedule": "",
20842086
},
20852087
}
2088+
assert changed_parameters == {
2089+
"vm_name": True,
2090+
"description": True,
2091+
"tags": False,
2092+
"memory": False,
2093+
"vcpu": True,
2094+
"power_state": False,
2095+
"snapshot_schedule": False,
2096+
}
20862097

20872098
def test_run_no_change(self, create_module, rest_client, mocker):
20882099
module = create_module(
@@ -2118,13 +2129,23 @@ def test_run_no_change(self, create_module, rest_client, mocker):
21182129
snapshot_schedule="",
21192130
)
21202131

2121-
changed, diff = ManageVMParams.set_vm_params(module, rest_client, vm_before, [])
2132+
changed, diff, changed_parameters = ManageVMParams.set_vm_params(
2133+
module, rest_client, vm_before, []
2134+
)
21222135

21232136
assert changed is False
21242137
assert diff == {
21252138
"before": None,
21262139
"after": None,
21272140
}
2141+
assert changed_parameters == {
2142+
"description": False,
2143+
"tags": False,
2144+
"memory": False,
2145+
"vcpu": False,
2146+
"power_state": False,
2147+
"snapshot_schedule": False,
2148+
}
21282149
assert vm_before.was_vm_rebooted() is False
21292150

21302151

tests/unit/plugins/modules/test_vm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ def test_ensure_present_update_record_no_changes(
812812
).return_value = None
813813
mocker.patch(
814814
"ansible_collections.scale_computing.hypercore.plugins.modules.vm._set_vm_params"
815-
).return_value = False
815+
).return_value = (False, {})
816816
mocker.patch(
817817
"ansible_collections.scale_computing.hypercore.plugins.modules.vm._set_disks"
818818
).return_value = False

0 commit comments

Comments
 (0)