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

1from __future__ import annotations 

2import shlex 

3from typing import Any 

4 

5import xmltodict 

6from kubernetes.dynamic.exceptions import ResourceNotFoundError 

7 

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 

13 

14 

15class VirtualMachineInstance(NamespacedResource): 

16 """ 

17 Virtual Machine Instance object, inherited from Resource. 

18 """ 

19 

20 api_group = NamespacedResource.ApiGroup.KUBEVIRT_IO 

21 

22 class Status(NamespacedResource.Status): 

23 SCHEDULING = "Scheduling" 

24 SCHEDULED = "Scheduled" 

25 

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 ) 

43 

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 ) 

51 

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 ) 

63 

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) 

68 

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) 

73 

74 @property 

75 def interfaces(self): 

76 return self.instance.status.interfaces 

77 

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}") 

89 

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] 

99 

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 

111 

112 raise ResourceNotFoundError 

113 

114 def wait_until_running(self, timeout=TIMEOUT_4MINUTES, logs=True, stop_status=None): 

115 """ 

116 Wait until VMI is running 

117 

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. 

122 

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 

139 

140 raise 

141 

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. 

146 

147 Args: 

148 pause (bool): True for paused, False for unpause 

149 timeout (int): Time to wait for the resource. 

150 

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) 

157 

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 

169 

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 

191 

192 @property 

193 def node(self): 

194 """ 

195 Get the node name where the VM is running 

196 

197 Returns: 

198 Node: Node 

199 """ 

200 return Node( 

201 client=self.client, 

202 name=self.instance.status.nodeName, 

203 ) 

204 

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 ) 

209 

210 def get_xml(self): 

211 """ 

212 Get virtual machine instance XML 

213 

214 Returns: 

215 xml_output(string): VMI XML in the multi-line string 

216 """ 

217 return self.execute_virsh_command(command="dumpxml") 

218 

219 @property 

220 def virt_launcher_pod_user_uid(self): 

221 """ 

222 Get Virt Launcher Pod User UID value 

223 

224 Returns: 

225 Int: Virt Launcher Pod UID value 

226 """ 

227 return self.virt_launcher_pod.instance.spec.securityContext.runAsUser 

228 

229 @property 

230 def is_virt_launcher_pod_root(self): 

231 """ 

232 Check if Virt Launcher Pod is Root 

233 

234 Returns: 

235 Bool: True if Virt Launcher Pod is Root. 

236 """ 

237 return not bool(self.virt_launcher_pod_user_uid) 

238 

239 @property 

240 def virt_launcher_pod_hypervisor_connection_uri(self): 

241 """ 

242 Get Virt Launcher Pod Hypervisor Connection URI 

243 

244 Required to connect to Hypervisor for 

245 Non-Root Virt-Launcher Pod. 

246 

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 

262 

263 def get_domstate(self): 

264 """ 

265 Get virtual machine instance Status. 

266 

267 Current workaround, as VM/VMI shows no status/phase == Paused yet. 

268 Bug: https://bugzilla.redhat.com/show_bug.cgi?id=1805178 

269 

270 Returns: 

271 String: VMI Status as string 

272 """ 

273 return self.execute_virsh_command(command="domstate") 

274 

275 def get_dommemstat(self): 

276 """ 

277 Get virtual machine domain memory stats 

278 link: https://libvirt.org/manpages/virsh.html#dommemstat 

279 

280 Returns: 

281 String: VMI domain memory stats as string 

282 """ 

283 return self.execute_virsh_command(command="dommemstat") 

284 

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 } 

294 

295 @property 

296 def xml_dict(self): 

297 """Get virtual machine instance XML as dict""" 

298 

299 return xmltodict.parse(xml_input=self.get_xml(), process_namespaces=True) 

300 

301 @property 

302 def guest_os_info(self): 

303 return self.api_request(method="GET", action="guestosinfo") 

304 

305 @property 

306 def guest_fs_info(self): 

307 return self.api_request(method="GET", action="filesystemlist") 

308 

309 @property 

310 def guest_user_info(self): 

311 return self.api_request(method="GET", action="userlist") 

312 

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 

319 

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 

323 

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 )