Coverage for ocp_resources/virtual_machine_instance.py: 0%
136 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 10:48 +0200
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 10:48 +0200
1from __future__ import annotations
2import shlex
3from typing import Any
5import xmltodict
6from kubernetes.dynamic.exceptions import ResourceNotFoundError
8from ocp_resources.utils.constants import PROTOCOL_ERROR_EXCEPTION_DICT, TIMEOUT_4MINUTES, TIMEOUT_30SEC, TIMEOUT_5SEC
9from ocp_resources.node import Node
10from ocp_resources.pod import Pod
11from ocp_resources.resource import NamespacedResource
12from timeout_sampler import TimeoutExpiredError, TimeoutSampler
15class VirtualMachineInstance(NamespacedResource):
16 """
17 Virtual Machine Instance object, inherited from Resource.
18 """
20 api_group = NamespacedResource.ApiGroup.KUBEVIRT_IO
22 class Status(NamespacedResource.Status):
23 SCHEDULING = "Scheduling"
24 SCHEDULED = "Scheduled"
26 def __init__(
27 self,
28 name=None,
29 namespace=None,
30 client=None,
31 yaml_file=None,
32 delete_timeout=TIMEOUT_4MINUTES,
33 **kwargs,
34 ):
35 super().__init__(
36 name=name,
37 namespace=namespace,
38 client=client,
39 yaml_file=yaml_file,
40 delete_timeout=delete_timeout,
41 **kwargs,
42 )
44 @property
45 def _subresource_api_url(self):
46 return (
47 f"{self.client.configuration.host}/"
48 f"apis/subresources.kubevirt.io/{self.api.api_version}/"
49 f"namespaces/{self.namespace}/virtualmachineinstances/{self.name}"
50 )
52 def api_request(
53 self, method: str, action: str, url: str = "", retry_params: dict[str, int] | None = None, **params: Any
54 ) -> dict[str, Any]:
55 default_vmi_api_request_retry_params: dict[str, int] = {"timeout": TIMEOUT_30SEC, "sleep_time": TIMEOUT_5SEC}
56 return super().api_request(
57 method=method,
58 action=action,
59 url=url or self._subresource_api_url,
60 retry_params=retry_params or default_vmi_api_request_retry_params,
61 **params,
62 )
64 def pause(self, timeout=TIMEOUT_4MINUTES, wait=False):
65 self.api_request(method="PUT", action="pause")
66 if wait:
67 return self.wait_for_pause_status(pause=True, timeout=timeout)
69 def unpause(self, timeout=TIMEOUT_4MINUTES, wait=False):
70 self.api_request(method="PUT", action="unpause")
71 if wait:
72 return self.wait_for_pause_status(pause=False, timeout=timeout)
74 @property
75 def interfaces(self):
76 return self.instance.status.interfaces
78 @property
79 def virt_launcher_pod(self):
80 pods = list(
81 Pod.get(
82 dyn_client=self.client,
83 namespace=self.namespace,
84 label_selector=f"kubevirt.io=virt-launcher,kubevirt.io/created-by={self.instance.metadata.uid}",
85 )
86 )
87 if not pods:
88 raise ResourceNotFoundError(f"VIRT launcher POD not found for {self.kind}:{self.name}")
90 migration_state = self.instance.status.migrationState
91 if migration_state:
92 # After VM migration there are two pods, one in Completed status and one in Running status.
93 # We need to return the Pod that is not in Completed status.
94 for pod in pods:
95 if migration_state.targetPod == pod.name:
96 return pod
97 else:
98 return pods[0]
100 @property
101 def virt_handler_pod(self):
102 pods = list(
103 Pod.get(
104 dyn_client=self.client,
105 label_selector="kubevirt.io=virt-handler",
106 )
107 )
108 for pod in pods:
109 if pod.instance["spec"]["nodeName"] == self.instance.status.nodeName:
110 return pod
112 raise ResourceNotFoundError
114 def wait_until_running(self, timeout=TIMEOUT_4MINUTES, logs=True, stop_status=None):
115 """
116 Wait until VMI is running
118 Args:
119 timeout (int): Time to wait for VMI.
120 logs (bool): True to extract logs from the VMI pod and from the VMI.
121 stop_status (str): Status which should stop the wait and failed.
123 Raises:
124 TimeoutExpiredError: If VMI failed to run.
125 """
126 try:
127 self.wait_for_status(status=self.Status.RUNNING, timeout=timeout, stop_status=stop_status)
128 except TimeoutExpiredError as sampler_ex:
129 if not logs:
130 raise
131 try:
132 virt_pod = self.virt_launcher_pod
133 self.logger.error(f"Status of virt-launcher pod {virt_pod.name}: {virt_pod.status}")
134 self.logger.debug(f"{virt_pod.name} *****LOGS*****")
135 self.logger.debug(virt_pod.log(container="compute"))
136 except ResourceNotFoundError as virt_pod_ex:
137 self.logger.error(virt_pod_ex)
138 raise sampler_ex
140 raise
142 def wait_for_pause_status(self, pause, timeout=TIMEOUT_4MINUTES):
143 """
144 Wait for Virtual Machine Instance to be paused / unpaused.
145 Paused status is checked in libvirt and in the VMI conditions.
147 Args:
148 pause (bool): True for paused, False for unpause
149 timeout (int): Time to wait for the resource.
151 Raises:
152 TimeoutExpiredError: If resource not exists.
153 """
154 self.logger.info(f"Wait until {self.kind} {self.name} is {'Paused' if pause else 'Unpuased'}")
155 self.wait_for_domstate_pause_status(pause=pause, timeout=timeout)
156 self.wait_for_vmi_condition_pause_status(pause=pause, timeout=timeout)
158 def wait_for_domstate_pause_status(self, pause, timeout=TIMEOUT_4MINUTES):
159 pause_status = "paused" if pause else "running"
160 samples = TimeoutSampler(
161 wait_timeout=timeout,
162 sleep=1,
163 exceptions_dict=PROTOCOL_ERROR_EXCEPTION_DICT,
164 func=self.get_domstate,
165 )
166 for sample in samples:
167 if pause_status in sample:
168 return
170 def wait_for_vmi_condition_pause_status(self, pause, timeout=TIMEOUT_4MINUTES):
171 samples = TimeoutSampler(
172 wait_timeout=timeout,
173 sleep=1,
174 exceptions_dict=PROTOCOL_ERROR_EXCEPTION_DICT,
175 func=self.get_vmi_active_condition,
176 )
177 for sample in samples:
178 # VM in state change
179 # We have commanded a [un]pause condition via the API but the CR has not been updated yet to match.
180 # 'reason' may not exist yet
181 # or
182 # 'reason' may still exist after unpause if the CR has not been updated before we perform this check
183 if (pause and not sample.get("reason")) or (sample.get("reason") == "PausedByUser" and not pause):
184 continue
185 # Paused VM
186 if pause and sample["reason"] == "PausedByUser":
187 return
188 # Unpaused VM
189 if not (pause and sample.get("reason")):
190 return
192 @property
193 def node(self):
194 """
195 Get the node name where the VM is running
197 Returns:
198 Node: Node
199 """
200 return Node(
201 client=self.client,
202 name=self.instance.status.nodeName,
203 )
205 def virsh_cmd(self, action):
206 return shlex.split(
207 f"virsh {self.virt_launcher_pod_hypervisor_connection_uri} {action} {self.namespace}_{self.name}"
208 )
210 def get_xml(self):
211 """
212 Get virtual machine instance XML
214 Returns:
215 xml_output(string): VMI XML in the multi-line string
216 """
217 return self.execute_virsh_command(command="dumpxml")
219 @property
220 def virt_launcher_pod_user_uid(self):
221 """
222 Get Virt Launcher Pod User UID value
224 Returns:
225 Int: Virt Launcher Pod UID value
226 """
227 return self.virt_launcher_pod.instance.spec.securityContext.runAsUser
229 @property
230 def is_virt_launcher_pod_root(self):
231 """
232 Check if Virt Launcher Pod is Root
234 Returns:
235 Bool: True if Virt Launcher Pod is Root.
236 """
237 return not bool(self.virt_launcher_pod_user_uid)
239 @property
240 def virt_launcher_pod_hypervisor_connection_uri(self):
241 """
242 Get Virt Launcher Pod Hypervisor Connection URI
244 Required to connect to Hypervisor for
245 Non-Root Virt-Launcher Pod.
247 Returns:
248 String: Hypervisor Connection URI
249 """
250 if self.is_virt_launcher_pod_root:
251 hypervisor_connection_uri = ""
252 else:
253 virtqemud_socket = "virtqemud"
254 socket = (
255 virtqemud_socket
256 if virtqemud_socket
257 in self.virt_launcher_pod.execute(command=["ls", "/var/run/libvirt/"], container="compute")
258 else "libvirt"
259 )
260 hypervisor_connection_uri = f"-c qemu+unix:///session?socket=/var/run/libvirt/{socket}-sock"
261 return hypervisor_connection_uri
263 def get_domstate(self):
264 """
265 Get virtual machine instance Status.
267 Current workaround, as VM/VMI shows no status/phase == Paused yet.
268 Bug: https://bugzilla.redhat.com/show_bug.cgi?id=1805178
270 Returns:
271 String: VMI Status as string
272 """
273 return self.execute_virsh_command(command="domstate")
275 def get_dommemstat(self):
276 """
277 Get virtual machine domain memory stats
278 link: https://libvirt.org/manpages/virsh.html#dommemstat
280 Returns:
281 String: VMI domain memory stats as string
282 """
283 return self.execute_virsh_command(command="dommemstat")
285 def get_vmi_active_condition(self):
286 """A VMI may have multiple conditions; the active one it the one with
287 'lastTransitionTime'"""
288 return {
289 k: v
290 for condition in self.instance.status.conditions
291 for k, v in condition.items()
292 if condition["lastTransitionTime"]
293 }
295 @property
296 def xml_dict(self):
297 """Get virtual machine instance XML as dict"""
299 return xmltodict.parse(xml_input=self.get_xml(), process_namespaces=True)
301 @property
302 def guest_os_info(self):
303 return self.api_request(method="GET", action="guestosinfo")
305 @property
306 def guest_fs_info(self):
307 return self.api_request(method="GET", action="filesystemlist")
309 @property
310 def guest_user_info(self):
311 return self.api_request(method="GET", action="userlist")
313 @property
314 def os_version(self):
315 vmi_os_version = self.instance.status.guestOSInfo.get("version", {})
316 if not vmi_os_version:
317 self.logger.warning("Guest agent is not installed on the VM; OS version is not available.")
318 return vmi_os_version
320 def interface_ip(self, interface):
321 iface_ip = [iface["ipAddress"] for iface in self.interfaces if iface["interfaceName"] == interface]
322 return iface_ip[0] if iface_ip else None
324 def execute_virsh_command(self, command):
325 return self.virt_launcher_pod.execute(
326 command=self.virsh_cmd(action=command),
327 container="compute",
328 )