Skip to content

Commit

Permalink
[GH-67] Implement the VNX metrics collection fwk
Browse files Browse the repository at this point in the history
Implement the VNX metrics/statistics collection/calculation framework
based on the work done on Unity.  Details below:
* Refactoring some of the common classes to common module.
* Add enable/disable statistics on VNXSystem
* Add enable/disable persist statistics on VNXSystem
* Setup calculator for VNX.
* Add metrics properties support for VNX's resource list.
* Add metrics dump support for VNX resources.

Add following metrics:
* VNXStorageProcessor
  * read_iops
  * write_iops
  * total_iops
  * read_mbps
  * write_mbps
  * total_mbps
  * read_size_kb
  * write_size_kb
* VNXLun
  * read_iops
    * read_iops_sp_a
    * read_iops_sp_b
  * write_iops
    * write_iops_sp_a
    * write_iops_sp_b
  * total_iops
  * read_mbps
    * read_mbps_sp_a
    * read_mbps_sp_b
  * write_mbps
    * write_mbps_sp_a
    * write_mbps_sp_b
  * total_mbps
  * implicit_trespasses_ps
    * implicit_trespasses_ps_sp_a
    * implicit_trespasses_ps_sp_b
  * explicit_trespasses_ps
    * explicit_trespasses_ps_sp_a
    * explicit_trespasses_ps_sp_b
  * utilization
    * utilization_sp_a
    * utilization_sp_b
  * read_size_kb
  * write_size_kb
* VNXDisk
  * read_iops
  * write_iops
  * total_iops
  * read_mbps
  * write_mbps
  * total_mbps
  * utilization
  * read_size_kb
  * write_size_kb
* VNXSPPort
  * read_iops
  * write_iops
  * total_iops
  * read_mbps
  * write_mbps
  * total_mbps
  * read_size_kb
  * write_size_kb
  • Loading branch information
Cedric Zhuang committed Dec 6, 2016
1 parent a1741d5 commit e21684e
Show file tree
Hide file tree
Showing 55 changed files with 33,345 additions and 518 deletions.
21 changes: 12 additions & 9 deletions storops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
import sys
import logging

from storops.vnx.resource.system import VNXSystem # noqa
from storops.unity.enums import * # noqa
from storops.unity.resource.system import UnitySystem # noqa
from storops.vnx.enums import * # noqa
from storops.unity.enums import * # noqa
from storops.vnx.resource.system import VNXSystem # noqa

__author__ = 'Cedric Zhuang'

Expand All @@ -30,14 +30,17 @@ def enable_log(level=logging.DEBUG):
"""Enable console logging.
This is a utils method for try run with storops.
:param level: log level, default to DEBUG
"""
log = logging.getLogger(__name__)
log.setLevel(level)
if not log.handlers:
log.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger(__name__)
logger.setLevel(level)
if not logger.handlers:
logger.info('enabling logging to console.')
logger.addHandler(logging.StreamHandler(sys.stdout))


def disable_log():
log = logging.getLogger(__name__)
log.setLevel(logging.NOTSET)
log.handlers = []
logger = logging.getLogger(__name__)
logger.info('disabling logging to console.')
logger.setLevel(logging.NOTSET)
logger.handlers = []
12 changes: 12 additions & 0 deletions storops/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,14 @@ class UnityMetricQueryNotFoundError(UnityMetricException):
error_code = 131153932


class VNXStatsError(VNXException):
pass


class VNXPerMonNotEnabledError(VNXStatsError):
pass


class NaviseccliNotAvailableError(VNXException):
message = ("naviseccli not found. please make sure it's installed"
" and available in path.")
Expand Down Expand Up @@ -896,6 +904,10 @@ class VNXUserNotFoundError(VNXSecurityException, VNXObjectNotFoundError):
error_message = 'User does not exist'


class VNXStatsException(VNXException):
pass


class VNXRaidGroupError(VNXException):
pass

Expand Down
4 changes: 4 additions & 0 deletions storops/lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,7 @@ def is_daemon(self):

def __del__(self):
self.stop()


def all_not_none(*items):
return all(map(lambda item: item is not None, items))
293 changes: 293 additions & 0 deletions storops/lib/metric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# coding=utf-8
# Copyright (c) 2016 EMC Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import unicode_literals

import os

import yaml

from storops.lib.common import RepeatedTimer, cache
from storops.lib.resource import ResourceList

__author__ = 'Cedric Zhuang'


class MetricCounterRecords(object):
""" Data structure to save metric counter in memory
"""

def __init__(self, maximum=None):
self.enabled = False
if maximum is None:
maximum = 2
self._maximum_len = maximum
self._records = []

def add_results(self, result):
self.enabled = True
if result is not None:
self._records.insert(0, result)
while len(self._records) > self._maximum_len:
self._records.pop()

def reset(self):
self.enabled = False
self._records = []

def __len__(self):
return len(self._records)

@property
def curr(self):
if self.enabled and len(self._records) > 0:
ret = self._records[0]
else:
ret = None
return ret

@property
def prev(self):
if self.enabled and len(self._records) > 1:
ret = self._records[1]
else:
ret = None
return ret


class PerfManager(object):
def __init__(self):
self.metric_collector = None
self._rsc_list_2 = None
self._rsc_clz_list = None
self.metric_counter_records = MetricCounterRecords()

def persist_rsc_list_metrics(self):
persist_rsc_list = self.get_persist_rsc_list()
if self.prev_counter and persist_rsc_list:
for rsc_list in persist_rsc_list:
rsc_list.update()
rsc_list.persist_metric_data()

def _is_perf_monitored(self, rsc):
if self._rsc_clz_list is not None:
if isinstance(rsc, ResourceList):
clz = rsc.get_resource_class()
else:
clz = type(rsc)
ret = clz in self._rsc_clz_list
else:
ret = True
return ret

def get_persist_rsc_list(self):
if self._rsc_list_2 is not None:
ret = [rsc_list
for rsc_list in self._rsc_list_2
if self.is_perf_metric_enabled(rsc_list)]
else:
ret = []
return ret

def enable_perf_metric(self, interval, callback, rsc_clz_list=None):
self._rsc_clz_list = rsc_clz_list

def f():
self.metric_counter_records.add_results(callback())
self.persist_rsc_list_metrics()

if self.metric_counter_records.enabled:
self.disable_perf_metric()

self.metric_counter_records.enabled = True
if interval > 0:
self.metric_collector = RepeatedTimer(interval, f)
self.metric_collector.start()

def disable_perf_metric(self):
if self.metric_collector:
self.metric_collector.stop()
self.metric_counter_records.reset()

def is_perf_metric_enabled(self, rsc=None):
ret = self.metric_counter_records.enabled
if rsc is not None and ret:
ret &= self._is_perf_monitored(rsc)
return ret

@property
def prev_counter(self):
return self.metric_counter_records.prev

@property
def curr_counter(self):
return self.metric_counter_records.curr

def __del__(self):
self.disable_perf_metric()

def persist_perf_stats(self, perf_rsc_list):
self._rsc_list_2 = perf_rsc_list

def is_perf_stats_persisted(self):
return self._rsc_list_2 is not None and len(self._rsc_list_2) > 0

def add_metric_record(self, record):
self.metric_counter_records.add_results(record)


class MetricConfigList(object):
def __init__(self, inputs):
if inputs is None:
inputs = []
self._metric_configs = {
config.get('name'): self.init_metric_config(config)
for config in inputs}

def init_metric_config(self, raw_config):
raise NotImplementedError('should be implemented by child class.'
'init config from dict.')

def metric_names(self):
return sorted(self._metric_configs.keys())

def get_calculator(self, metric_name):
return self.get_metric_config(metric_name).calculator

def get_metric_config(self, metric_name):
if metric_name not in self._metric_configs:
raise ValueError('calculator for metric "{}" not found in {}.'
.format(metric_name, self._metric_configs))
return self._metric_configs[metric_name]

def paths(self):
return [path
for config in self._metric_configs.values()
for path in config.paths]


class MetricConfigParser(object):
config_filename = 'metric_configs.yaml'

@classmethod
def get_folder(cls):
raise NotImplementedError('should be implemented by child class.'
'returns the folder of metric_configs.yaml')

@classmethod
def get_config(cls, name):
raise NotImplementedError('should be implemented by child class.'
'return the metric config instance.')

@classmethod
@cache
def _read_configs(cls):
filename = os.path.join(cls.get_folder(), cls.config_filename)
with open(filename, 'r') as stream:
ret = yaml.load(stream)
return ret

@classmethod
def _get_clz_name(cls, name):
if isinstance(name, type):
name = name.__name__.split('.')[-1]
return name


class CalculatorMetaInfo(object):
def __init__(self):
self._metric_config = self.get_config_parser()

def get_config_parser(self):
raise NotImplementedError('should be implemented by child class.'
'return the config parser class.')

def get_calculator(self, clz, metric_name):
config_list = self.get_config(clz)
return config_list.get_calculator(metric_name)

def get_config(self, clz):
return self._metric_config.get_config(clz)

def get_metric_names(self, clz):
return self.get_config(clz).metric_names()

def get_metric_value(self, clz, metric_name, cli, obj=None):
raise NotImplementedError('should be implemented by child class.'
'return the calculated metric value.')


class MetricsDumper(object):
def __init__(self, rsc_list, dft_hdr=None, dft_hdr_cb=None):
if dft_hdr is None:
dft_hdr = []
if dft_hdr_cb is not None and not callable(dft_hdr_cb):
raise TypeError('dft_hdr_cb should be a function takes in a rsc '
'and returns a string list.')

self._rsc_list = rsc_list
self._dft_hdr = dft_hdr
self._dft_hdr_cb = dft_hdr_cb

@property
def metric_names(self):
return self._rsc_list.metric_names()

def get_metrics_csv(self, sep=None):
if sep is None:
sep = ','
content = [self.get_metrics_csv_header(sep),
self.get_metrics_csv_data(sep)]
return '\n'.join(content)

def data_line(self, rsc):
if self._dft_hdr_cb is not None:
metrics = self._dft_hdr_cb(rsc)
else:
metrics = []
metrics += [str(self.get_attr(rsc, name))
for name in self.metric_names]
return metrics

@staticmethod
def get_attr(rsc, name):
if hasattr(rsc, name):
ret = getattr(rsc, name)
else:
ret = rsc.get(name)
return ret

def get_metrics_csv_data(self, sep=None):
if sep is None:
sep = ','
return '\n'.join(sep.join(self.data_line(r)) for r in self._rsc_list)

def get_metrics_csv_header(self, sep=None):
if sep is None:
sep = ','
return sep.join(self._dft_hdr + self.metric_names)

def persist_metric_data(self, filename=None):
if filename is None:
raise ValueError('filename should not be none.')

if os.path.exists(filename):
to_write = self.get_metrics_csv_data()
else:
to_write = self.get_metrics_csv()

with open(filename, 'a+') as f:
f.write(to_write)
f.write('\n')
Loading

0 comments on commit e21684e

Please sign in to comment.