1# Copyright 2016 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"""
17Utility class for metrics related operations.
18Based on the "root/virtualization/v2" namespace available starting with
19Hyper-V Server / Windows Server 2012.
20"""
21
22from oslo_log import log as logging
23
24from os_win._i18n import _
25from os_win import exceptions
26from os_win.utils import _wqlutils
27from os_win.utils import baseutils
28
29LOG = logging.getLogger(__name__)
30
31
32class MetricsUtils(baseutils.BaseUtilsVirt):
33
34    _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized'
35    _DVD_DISK_RES_SUB_TYPE = 'Microsoft:Hyper-V:Virtual CD/DVD Disk'
36    _STORAGE_ALLOC_SETTING_DATA_CLASS = 'Msvm_StorageAllocationSettingData'
37    _PROCESSOR_SETTING_DATA_CLASS = 'Msvm_ProcessorSettingData'
38    _SYNTH_ETH_PORT_SET_DATA = 'Msvm_SyntheticEthernetPortSettingData'
39    _PORT_ALLOC_SET_DATA = 'Msvm_EthernetPortAllocationSettingData'
40    _PORT_ALLOC_ACL_SET_DATA = 'Msvm_EthernetSwitchPortAclSettingData'
41    _BASE_METRICS_VALUE = 'Msvm_BaseMetricValue'
42
43    _CPU_METRICS = 'Aggregated Average CPU Utilization'
44    _MEMORY_METRICS = 'Aggregated Average Memory Utilization'
45    _NET_IN_METRICS = 'Filtered Incoming Network Traffic'
46    _NET_OUT_METRICS = 'Filtered Outgoing Network Traffic'
47    # Disk metrics are supported from Hyper-V 2012 R2
48    _DISK_RD_METRICS = 'Disk Data Read'
49    _DISK_WR_METRICS = 'Disk Data Written'
50    _DISK_LATENCY_METRICS = 'Average Disk Latency'
51    _DISK_IOPS_METRICS = 'Average Normalized Disk Throughput'
52
53    _METRICS_ENABLED = 2
54
55    def __init__(self, host='.'):
56        super(MetricsUtils, self).__init__(host)
57        self._metrics_svc_obj = None
58        self._metrics_defs_obj = {}
59
60        # We need to avoid a circular dependency.
61        from os_win import utilsfactory
62        self._vmutils = utilsfactory.get_vmutils(host)
63
64    @property
65    def _metrics_svc(self):
66        if not self._metrics_svc_obj:
67            self._metrics_svc_obj = self._compat_conn.Msvm_MetricService()[0]
68        return self._metrics_svc_obj
69
70    @property
71    def _metrics_defs(self):
72        if not self._metrics_defs_obj:
73            self._cache_metrics_defs()
74        return self._metrics_defs_obj
75
76    def _cache_metrics_defs(self):
77        for metrics_def in self._conn.CIM_BaseMetricDefinition():
78            self._metrics_defs_obj[metrics_def.ElementName] = metrics_def
79
80    def enable_vm_metrics_collection(self, vm_name):
81        vm = self._get_vm(vm_name)
82        disks = self._get_vm_resources(vm_name,
83                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
84        filtered_disks = [d for d in disks if
85                          d.ResourceSubType != self._DVD_DISK_RES_SUB_TYPE]
86
87        # enable metrics for disk.
88        for disk in filtered_disks:
89            self._enable_metrics(disk)
90
91        metrics_names = [self._CPU_METRICS, self._MEMORY_METRICS]
92        self._enable_metrics(vm, metrics_names)
93
94    def enable_disk_metrics_collection(self, attached_disk_path=None,
95                                       is_physical=False,
96                                       serial=None):
97        disk = self._vmutils._get_mounted_disk_resource_from_path(
98            attached_disk_path, is_physical=is_physical, serial=serial)
99        self._enable_metrics(disk)
100
101    def enable_port_metrics_collection(self, switch_port_name):
102        port = self._get_switch_port(switch_port_name)
103        metrics_names = [self._NET_IN_METRICS, self._NET_OUT_METRICS]
104        self._enable_metrics(port, metrics_names)
105
106    def _enable_metrics(self, element, metrics_names=None):
107        if not metrics_names:
108            definition_paths = [None]
109        else:
110            definition_paths = []
111            for metrics_name in metrics_names:
112                metrics_def = self._metrics_defs.get(metrics_name)
113                if not metrics_def:
114                    LOG.warning("Metric not found: %s", metrics_name)
115                    continue
116                definition_paths.append(metrics_def.path_())
117
118        element_path = element.path_()
119        for definition_path in definition_paths:
120            ret_val = self._metrics_svc.ControlMetrics(
121                Subject=element_path,
122                Definition=definition_path,
123                MetricCollectionEnabled=self._METRICS_ENABLED)[0]
124            if ret_val:
125                err_msg = _("Failed to enable metrics for resource "
126                            "%(resource_name)s. "
127                            "Return code: %(ret_val)s.") % dict(
128                                resource_name=element.ElementName,
129                                ret_val=ret_val)
130                raise exceptions.OSWinException(err_msg)
131
132    def get_cpu_metrics(self, vm_name):
133        vm = self._get_vm(vm_name)
134        cpu_sd = self._get_vm_resources(vm_name,
135                                        self._PROCESSOR_SETTING_DATA_CLASS)[0]
136        cpu_metrics_def = self._metrics_defs[self._CPU_METRICS]
137        cpu_metrics_aggr = self._get_metrics(vm, cpu_metrics_def)
138
139        cpu_used = 0
140        if cpu_metrics_aggr:
141            cpu_used = int(cpu_metrics_aggr[0].MetricValue)
142
143        return (cpu_used,
144                int(cpu_sd.VirtualQuantity),
145                int(vm.OnTimeInMilliseconds))
146
147    def get_memory_metrics(self, vm_name):
148        vm = self._get_vm(vm_name)
149        memory_def = self._metrics_defs[self._MEMORY_METRICS]
150        metrics_memory = self._get_metrics(vm, memory_def)
151        memory_usage = 0
152        if metrics_memory:
153            memory_usage = int(metrics_memory[0].MetricValue)
154        return memory_usage
155
156    def get_vnic_metrics(self, vm_name):
157        ports = self._get_vm_resources(vm_name, self._PORT_ALLOC_SET_DATA)
158        vnics = self._get_vm_resources(vm_name, self._SYNTH_ETH_PORT_SET_DATA)
159
160        metrics_def_in = self._metrics_defs[self._NET_IN_METRICS]
161        metrics_def_out = self._metrics_defs[self._NET_OUT_METRICS]
162
163        for port in ports:
164            vnic = [v for v in vnics if port.Parent == v.path_()][0]
165            port_acls = _wqlutils.get_element_associated_class(
166                self._conn, self._PORT_ALLOC_ACL_SET_DATA,
167                element_instance_id=port.InstanceID)
168
169            metrics_value_instances = self._get_metrics_value_instances(
170                port_acls, self._BASE_METRICS_VALUE)
171            metrics_values = self._sum_metrics_values_by_defs(
172                metrics_value_instances, [metrics_def_in, metrics_def_out])
173
174            yield {
175                'rx_mb': metrics_values[0],
176                'tx_mb': metrics_values[1],
177                'element_name': vnic.ElementName,
178                'address': vnic.Address
179            }
180
181    def get_disk_metrics(self, vm_name):
182        metrics_def_r = self._metrics_defs[self._DISK_RD_METRICS]
183        metrics_def_w = self._metrics_defs[self._DISK_WR_METRICS]
184
185        disks = self._get_vm_resources(vm_name,
186                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
187        for disk in disks:
188            metrics_values = self._get_metrics_values(
189                disk, [metrics_def_r, metrics_def_w])
190
191            yield {
192                # Values are in megabytes
193                'read_mb': metrics_values[0],
194                'write_mb': metrics_values[1],
195                'instance_id': disk.InstanceID,
196                'host_resource': disk.HostResource[0]
197            }
198
199    def get_disk_latency_metrics(self, vm_name):
200        metrics_latency_def = self._metrics_defs[self._DISK_LATENCY_METRICS]
201
202        disks = self._get_vm_resources(vm_name,
203                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
204        for disk in disks:
205            metrics_values = self._get_metrics_values(
206                disk, [metrics_latency_def])
207
208            yield {
209                'disk_latency': metrics_values[0],
210                'instance_id': disk.InstanceID,
211            }
212
213    def get_disk_iops_count(self, vm_name):
214        metrics_def_iops = self._metrics_defs[self._DISK_IOPS_METRICS]
215
216        disks = self._get_vm_resources(vm_name,
217                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
218        for disk in disks:
219            metrics_values = self._get_metrics_values(
220                disk, [metrics_def_iops])
221
222            yield {
223                'iops_count': metrics_values[0],
224                'instance_id': disk.InstanceID,
225            }
226
227    @staticmethod
228    def _sum_metrics_values(metrics):
229        return sum([int(metric.MetricValue) for metric in metrics])
230
231    def _sum_metrics_values_by_defs(self, element_metrics, metrics_defs):
232        metrics_values = []
233        for metrics_def in metrics_defs:
234            if metrics_def:
235                metrics = self._filter_metrics(element_metrics, metrics_def)
236                metrics_values.append(self._sum_metrics_values(metrics))
237            else:
238                # In case the metric is not defined on this host
239                metrics_values.append(0)
240        return metrics_values
241
242    def _get_metrics_value_instances(self, elements, result_class):
243        instances = []
244        for el in elements:
245            # NOTE(abalutoiu): Msvm_MetricForME is the association between
246            # an element and all the metric values maintained for it.
247            el_metric = [
248                x.Dependent for x in self._conn.Msvm_MetricForME(
249                    Antecedent=el.path_())]
250            el_metric = [
251                x for x in el_metric if x.path().Class == result_class]
252            if el_metric:
253                instances.append(el_metric[0])
254
255        return instances
256
257    def _get_metrics_values(self, element, metrics_defs):
258        element_metrics = [
259            x.Dependent for x in self._conn.Msvm_MetricForME(
260                Antecedent=element.path_())]
261        return self._sum_metrics_values_by_defs(element_metrics, metrics_defs)
262
263    def _get_metrics(self, element, metrics_def):
264        metrics = [
265            x.Dependent for x in self._conn.Msvm_MetricForME(
266                Antecedent=element.path_())]
267        return self._filter_metrics(metrics, metrics_def)
268
269    @staticmethod
270    def _filter_metrics(all_metrics, metrics_def):
271        return [v for v in all_metrics if
272                v.MetricDefinitionId == metrics_def.Id]
273
274    def _get_vm_resources(self, vm_name, resource_class):
275        setting_data = self._get_vm_setting_data(vm_name)
276        return _wqlutils.get_element_associated_class(
277            self._conn, resource_class,
278            element_instance_id=setting_data.InstanceID)
279
280    def _get_vm(self, vm_name):
281        vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name)
282        return self._unique_result(vms, vm_name)
283
284    def _get_switch_port(self, port_name):
285        ports = self._conn.Msvm_EthernetPortAllocationSettingData(
286            ElementName=port_name)
287        return self._unique_result(ports, port_name)
288
289    def _get_vm_setting_data(self, vm_name):
290        vssds = self._conn.Msvm_VirtualSystemSettingData(
291            ElementName=vm_name)
292        return self._unique_result(vssds, vm_name)
293
294    @staticmethod
295    def _unique_result(objects, resource_name):
296        n = len(objects)
297        if n == 0:
298            raise exceptions.NotFound(resource=resource_name)
299        elif n > 1:
300            raise exceptions.OSWinException(
301                _('Duplicate resource name found: %s') % resource_name)
302        else:
303            return objects[0]
304