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