1# Copyright 2015 Cloudbase Solutions Srl 2# 3# All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16 17""" 18Base Utility class for operations on Hyper-V. 19""" 20 21import time 22 23from oslo_log import log as logging 24 25from os_win import _utils 26import os_win.conf 27from os_win import constants 28from os_win import exceptions 29from os_win.utils import baseutils 30 31CONF = os_win.conf.CONF 32 33LOG = logging.getLogger(__name__) 34 35 36class JobUtils(baseutils.BaseUtilsVirt): 37 38 _CONCRETE_JOB_CLASS = "Msvm_ConcreteJob" 39 40 _KILL_JOB_STATE_CHANGE_REQUEST = 5 41 42 _completed_job_states = [constants.JOB_STATE_COMPLETED, 43 constants.JOB_STATE_TERMINATED, 44 constants.JOB_STATE_KILLED, 45 constants.JOB_STATE_COMPLETED_WITH_WARNINGS, 46 constants.JOB_STATE_EXCEPTION] 47 _successful_job_states = [constants.JOB_STATE_COMPLETED, 48 constants.JOB_STATE_COMPLETED_WITH_WARNINGS] 49 50 def check_ret_val(self, ret_val, job_path, success_values=[0]): 51 """Checks that the job represented by the given arguments succeeded. 52 53 Some Hyper-V operations are not atomic, and will return a reference 54 to a job. In this case, this method will wait for the job's 55 completion. 56 57 :param ret_val: integer, representing the return value of the job. 58 if the value is WMI_JOB_STATUS_STARTED or WMI_JOB_STATE_RUNNING, 59 a job_path cannot be None. 60 :param job_path: string representing the WMI object path of a 61 Hyper-V job. 62 :param success_values: list of return values that can be considered 63 successful. WMI_JOB_STATUS_STARTED and WMI_JOB_STATE_RUNNING 64 values are ignored. 65 :raises exceptions.WMIJobFailed: if the given ret_val is 66 WMI_JOB_STATUS_STARTED or WMI_JOB_STATE_RUNNING and the state of 67 job represented by the given job_path is not 68 WMI_JOB_STATE_COMPLETED or JOB_STATE_COMPLETED_WITH_WARNINGS, or 69 if the given ret_val is not in the list of given success_values. 70 """ 71 if ret_val in [constants.WMI_JOB_STATUS_STARTED, 72 constants.WMI_JOB_STATE_RUNNING]: 73 return self._wait_for_job(job_path) 74 elif ret_val not in success_values: 75 raise exceptions.WMIJobFailed(error_code=ret_val, 76 job_state=None, 77 error_summ_desc=None, 78 error_desc=None) 79 80 def _wait_for_job(self, job_path): 81 """Poll WMI job state and wait for completion.""" 82 83 job_wmi_path = job_path.replace('\\', '/') 84 job = self._get_wmi_obj(job_wmi_path) 85 86 # We'll log the job status from time to time. 87 last_report_time = 0 88 report_interval = 5 89 90 while not self._is_job_completed(job): 91 now = time.monotonic() 92 if now - last_report_time > report_interval: 93 job_details = self._get_job_details(job) 94 LOG.debug("Waiting for WMI job: %s.", job_details) 95 last_report_time = now 96 97 time.sleep(0.1) 98 job = self._get_wmi_obj(job_wmi_path) 99 100 job_state = job.JobState 101 err_code = job.ErrorCode 102 103 # We'll raise an exception for killed jobs. 104 job_failed = job_state not in self._successful_job_states or err_code 105 job_warnings = job_state == constants.JOB_STATE_COMPLETED_WITH_WARNINGS 106 job_details = self._get_job_details( 107 job, extended=(job_failed or job_warnings)) 108 109 if job_failed: 110 err_sum_desc = getattr(job, 'ErrorSummaryDescription', None) 111 err_desc = job.ErrorDescription 112 113 LOG.error("WMI job failed: %s.", job_details) 114 raise exceptions.WMIJobFailed(job_state=job_state, 115 error_code=err_code, 116 error_summ_desc=err_sum_desc, 117 error_desc=err_desc) 118 119 if job_warnings: 120 LOG.warning("WMI job completed with warnings. For detailed " 121 "information, please check the Windows event logs. " 122 "Job details: %s.", job_details) 123 else: 124 LOG.debug("WMI job succeeded: %s.", job_details) 125 126 return job 127 128 def _get_job_error_details(self, job): 129 try: 130 return job.GetErrorEx() 131 except Exception: 132 LOG.error("Could not get job '%s' error details.", job.InstanceID) 133 134 def _get_job_details(self, job, extended=False): 135 basic_details = [ 136 "InstanceID", "Description", "ElementName", "JobStatus", 137 "ElapsedTime", "Cancellable", "JobType", "Owner", 138 "PercentComplete"] 139 extended_details = [ 140 "JobState", "StatusDescriptions", "OperationalStatus", 141 "TimeSubmitted", "UntilTime", "TimeOfLastStateChange", 142 "DetailedStatus", "LocalOrUtcTime", 143 "ErrorCode", "ErrorDescription", "ErrorSummaryDescription"] 144 145 fields = list(basic_details) 146 details = {} 147 148 if extended: 149 fields += extended_details 150 err_details = self._get_job_error_details(job) 151 details['RawErrors'] = err_details 152 153 for field in fields: 154 try: 155 details[field] = getattr(job, field) 156 except AttributeError: 157 continue 158 159 return details 160 161 def _get_pending_jobs_affecting_element(self, element): 162 # Msvm_AffectedJobElement is in fact an association between 163 # the affected element and the affecting job. 164 mappings = self._conn.Msvm_AffectedJobElement( 165 AffectedElement=element.path_()) 166 pending_jobs = [] 167 for mapping in mappings: 168 try: 169 if mapping.AffectingElement and not self._is_job_completed( 170 mapping.AffectingElement): 171 pending_jobs.append(mapping.AffectingElement) 172 173 except exceptions.x_wmi as ex: 174 # NOTE(claudiub): we can ignore "Not found" type exceptions. 175 if not _utils._is_not_found_exc(ex): 176 raise 177 178 return pending_jobs 179 180 def _stop_jobs(self, element): 181 pending_jobs = self._get_pending_jobs_affecting_element(element) 182 for job in pending_jobs: 183 job_details = self._get_job_details(job, extended=True) 184 try: 185 if not job.Cancellable: 186 LOG.debug("Got request to terminate " 187 "non-cancelable job: %s.", job_details) 188 continue 189 190 job.RequestStateChange( 191 self._KILL_JOB_STATE_CHANGE_REQUEST) 192 except exceptions.x_wmi as ex: 193 # The job may had been completed right before we've 194 # attempted to kill it. 195 if not _utils._is_not_found_exc(ex): 196 LOG.debug("Failed to stop job. Exception: %s. " 197 "Job details: %s.", ex, job_details) 198 199 pending_jobs = self._get_pending_jobs_affecting_element(element) 200 if pending_jobs: 201 pending_job_details = [self._get_job_details(job, extended=True) 202 for job in pending_jobs] 203 LOG.debug("Attempted to terminate jobs " 204 "affecting element %(element)s but " 205 "%(pending_count)s jobs are still pending: " 206 "%(pending_jobs)s.", 207 dict(element=element, 208 pending_count=len(pending_jobs), 209 pending_jobs=pending_job_details)) 210 raise exceptions.JobTerminateFailed() 211 212 def _is_job_completed(self, job): 213 return job.JobState in self._completed_job_states 214 215 def stop_jobs(self, element, timeout=None): 216 """Stops the Hyper-V jobs associated with the given resource. 217 218 :param element: string representing the path of the Hyper-V resource 219 whose jobs will be stopped. 220 :param timeout: the maximum amount of time allowed to stop all the 221 given resource's jobs. 222 :raises exceptions.JobTerminateFailed: if there are still pending jobs 223 associated with the given resource and the given timeout amount of 224 time has passed. 225 """ 226 if timeout is None: 227 timeout = CONF.os_win.wmi_job_terminate_timeout 228 229 @_utils.retry_decorator(exceptions=exceptions.JobTerminateFailed, 230 timeout=timeout, max_retry_count=None) 231 def _stop_jobs_with_timeout(): 232 self._stop_jobs(element) 233 234 _stop_jobs_with_timeout() 235 236 @_utils.not_found_decorator() 237 @_utils.retry_decorator(exceptions=exceptions.HyperVException) 238 def add_virt_resource(self, virt_resource, parent): 239 (job_path, new_resources, 240 ret_val) = self._vs_man_svc.AddResourceSettings( 241 parent.path_(), [virt_resource.GetText_(1)]) 242 self.check_ret_val(ret_val, job_path) 243 return new_resources 244 245 # modify_virt_resource can fail, especially while setting up the VM's 246 # serial port connection. Retrying the operation will yield success. 247 @_utils.not_found_decorator() 248 @_utils.retry_decorator(exceptions=exceptions.HyperVException) 249 def modify_virt_resource(self, virt_resource): 250 (job_path, out_set_data, 251 ret_val) = self._vs_man_svc.ModifyResourceSettings( 252 ResourceSettings=[virt_resource.GetText_(1)]) 253 self.check_ret_val(ret_val, job_path) 254 255 def remove_virt_resource(self, virt_resource): 256 self.remove_multiple_virt_resources([virt_resource]) 257 258 @_utils.not_found_decorator() 259 @_utils.retry_decorator(exceptions=exceptions.HyperVException) 260 def remove_multiple_virt_resources(self, virt_resources): 261 (job, ret_val) = self._vs_man_svc.RemoveResourceSettings( 262 ResourceSettings=[r.path_() for r in virt_resources]) 263 self.check_ret_val(ret_val, job) 264 265 def add_virt_feature(self, virt_feature, parent): 266 self.add_multiple_virt_features([virt_feature], parent) 267 268 @_utils.not_found_decorator() 269 @_utils.retry_decorator(exceptions=exceptions.HyperVException) 270 def add_multiple_virt_features(self, virt_features, parent): 271 (job_path, out_set_data, 272 ret_val) = self._vs_man_svc.AddFeatureSettings( 273 parent.path_(), [f.GetText_(1) for f in virt_features]) 274 self.check_ret_val(ret_val, job_path) 275 276 @_utils.not_found_decorator() 277 @_utils.retry_decorator(exceptions=exceptions.HyperVException) 278 def modify_virt_feature(self, virt_feature): 279 (job_path, out_set_data, 280 ret_val) = self._vs_man_svc.ModifyFeatureSettings( 281 FeatureSettings=[virt_feature.GetText_(1)]) 282 self.check_ret_val(ret_val, job_path) 283 284 def remove_virt_feature(self, virt_feature): 285 self.remove_multiple_virt_features([virt_feature]) 286 287 @_utils.not_found_decorator() 288 def remove_multiple_virt_features(self, virt_features): 289 (job_path, ret_val) = self._vs_man_svc.RemoveFeatureSettings( 290 FeatureSettings=[f.path_() for f in virt_features]) 291 self.check_ret_val(ret_val, job_path) 292