From 37a9950ab20450c4b642f09ac41ddd7ae100e31e Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Thu, 9 Mar 2017 09:48:17 -0600 Subject: [PATCH] Added VM instances used and quotas (#6) The following plugins add a basic OpenStack API framework which is used to pull metrics for nova vm quotas and usage for RAM, vCPUs, disk, and instance count. added codecov.yml Signed-off-by: Kevin Carter --- AUTHORS | 1 + ChangeLog | 3 +- codecov.yml | 4 + etc/openstack.ini | 47 ++++++++++ monitorstack/cli.py | 5 ++ monitorstack/common/formatters.py | 60 ++++++++----- monitorstack/plugins/os_vm_quota_cores.py | 59 ++++++++++++ monitorstack/plugins/os_vm_quota_instance.py | 59 ++++++++++++ monitorstack/plugins/os_vm_quota_ram.py | 59 ++++++++++++ monitorstack/plugins/os_vm_used_cores.py | 63 +++++++++++++ monitorstack/plugins/os_vm_used_disk.py | 63 +++++++++++++ monitorstack/plugins/os_vm_used_instance.py | 59 ++++++++++++ monitorstack/plugins/os_vm_used_ram.py | 63 +++++++++++++ monitorstack/plugins/uptime.py | 2 + monitorstack/utils/__init__.py | 95 ++++++++++++++++++++ monitorstack/utils/os_utils.py | 90 +++++++++++++++++++ test-requirements.txt | 1 + tests/__init__.py | 0 tests/test_formatters.py | 2 +- tests/test_os_vm.py | 62 +++++++++++++ tests/test_plugin_kvm.py | 2 +- tests/test_utils.py | 70 +++++++++++++++ 22 files changed, 842 insertions(+), 27 deletions(-) create mode 100644 codecov.yml create mode 100644 etc/openstack.ini create mode 100644 monitorstack/plugins/os_vm_quota_cores.py create mode 100644 monitorstack/plugins/os_vm_quota_instance.py create mode 100644 monitorstack/plugins/os_vm_quota_ram.py create mode 100644 monitorstack/plugins/os_vm_used_cores.py create mode 100644 monitorstack/plugins/os_vm_used_disk.py create mode 100644 monitorstack/plugins/os_vm_used_instance.py create mode 100644 monitorstack/plugins/os_vm_used_ram.py create mode 100644 monitorstack/utils/__init__.py create mode 100644 monitorstack/utils/os_utils.py create mode 100644 tests/__init__.py create mode 100644 tests/test_os_vm.py create mode 100644 tests/test_utils.py diff --git a/AUTHORS b/AUTHORS index 15ae75e..ddb2454 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Kevin Carter +Kevin Carter Major Hayden diff --git a/ChangeLog b/ChangeLog index 53e9b32..07caf1e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,8 @@ CHANGES ======= -* Improve test coverage +* Added VM instances used and quotas +* Improve test coverage (#4) * Remove sys from uptime test (#3) * Increase test coverage for uptime * Added KVM Metric plugin (#2) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..037a203 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + round: down + range: 70..80 + precision: 5 diff --git a/etc/openstack.ini b/etc/openstack.ini new file mode 100644 index 0000000..c16f32d --- /dev/null +++ b/etc/openstack.ini @@ -0,0 +1,47 @@ +# Store the authentication credentials needed to query a given OpenStack Service. +# All sections are overrides for the defaults. If you only need to connect to a +# single cloud simply store the credentials needd in the DEFAULT section and +# override whatever is needed within the local sections. + +[DEFAULT] +insecure = false +auth_url = https://example.com:5000/v3 + +# NOTE(cloudnull): +# When using keystone V3 you will need the .*domain_name configuration options. +user_domain_name = default # This is required when Keystone V3 is being used +project_domain_name = default # This is required when Keystone V3 is being used + +# If you're using keystone V2 you will need the tenant_name option. +tenant_name = admin # This is required when Keystone V2 is being used + +# NEVER Mix and match the options tenant name and domain_name options. +# You are be required to run either V2 or V3 as it pertains to this config. +# If you provide both tenant_name and .*domain_name options at the same time +# the plugins will fail API version negotiation. + +username = admin +password = Secrete +# The verify option is for SSL. If your SSL certificate is not +# valid set this option to false else omit it or set it true. +verify = false + +[keystone] + +[glance] + +[nova] +project_name = nova + +[neutron] + +[heat] + +[cinder] + +[ironic] +auth_url = https://example2.com:5000/v3 +project_name = ironic +user_domain_name = users +project_domain_name = projects +password = SuperSecrete diff --git a/monitorstack/cli.py b/monitorstack/cli.py index ef0e4f8..a45e1fe 100755 --- a/monitorstack/cli.py +++ b/monitorstack/cli.py @@ -51,6 +51,7 @@ class MonitorStackCLI(click.MultiCommand): @property def cmd_folder(self): """Get the path to the plugin directory.""" + return os.path.abspath( os.path.join( os.path.dirname(__file__), @@ -60,6 +61,7 @@ class MonitorStackCLI(click.MultiCommand): def list_commands(self, ctx): """Get a list of all available commands.""" + rv = list() for _, pkg_name, _ in pkgutil.iter_modules([self.cmd_folder]): rv.append(pkg_name) @@ -68,6 +70,7 @@ class MonitorStackCLI(click.MultiCommand): def get_command(self, ctx, name): """Load a command and run it.""" + for _, pkg_name, _ in pkgutil.iter_modules([self.cmd_folder]): if pkg_name == name: mod = importlib.import_module( @@ -99,6 +102,7 @@ VALID_OUTPUT_FORMATS = [ @pass_context def cli(ctx, output_format, verbose): """A complex command line interface.""" + ctx.verbose = verbose pass @@ -106,6 +110,7 @@ def cli(ctx, output_format, verbose): @cli.resultcallback(replace=True) def process_result(result, output_format, verbose): """Render the output into the proper format.""" + module_name = 'monitorstack.common.formatters' method_name = 'write_{}'.format(output_format) output_formatter = getattr( diff --git a/monitorstack/common/formatters.py b/monitorstack/common/formatters.py index f4ca3b7..894571e 100644 --- a/monitorstack/common/formatters.py +++ b/monitorstack/common/formatters.py @@ -18,13 +18,9 @@ import time import click -def current_time(): - """Return the current time in nanoseconds.""" - return int(time.time() * 1000000000) - - def write_json(result): """Output in raw JSON format.""" + output = json.dumps(result, indent=2) click.echo(output) return True @@ -32,36 +28,52 @@ def write_json(result): def write_line(result): """Output in line format.""" + for key, value in result['variables'].items(): click.echo("{} {}".format(key, value)) return True +def _current_time(): + """Return the current time in nanoseconds.""" + + return int(time.time() * 1000000000) + + +def _telegraf_line_format(sets, quote=False): + """Return a comma separated string.""" + + store = list() + for k, v in sets.items(): + k = k.replace(' ', '_') + for v_type in [int, float]: + try: + v = v_type(v) + except ValueError: + pass # v was not a int, float, or long + else: + break + if not isinstance(v, (int, float, bool)) and quote: + store.append('{}="{}"'.format(k, v)) + else: + store.append('{}={}'.format(k, v)) + return ','.join(store).rstrip(',') + + def write_telegraf(result): """Output in telegraf format.""" - def line_format(sets, quote=False): - store = list() - for k, v in sets.items(): - k = k.replace(' ', '_') - for v_type in [int, float]: - try: - v = v_type(v) - except ValueError: - pass # v was not a int, float, or long - else: - break - if not isinstance(v, (int, float, bool)) and quote: - store.append('{}="{}"'.format(k, v)) - else: - store.append('{}={}'.format(k, v)) - return ','.join(store).rstrip(',') resultant = [result['measurement_name']] if 'meta' in result: - resultant.append(line_format(sets=result['meta'])) - resultant.append(line_format(sets=result['variables'], quote=True)) - resultant.append(str(current_time())) + resultant.append(_telegraf_line_format(sets=result['meta'])) + resultant.append( + _telegraf_line_format( + sets=result['variables'], + quote=True + ) + ) + resultant.append(str(_current_time())) click.echo(' '.join(resultant)) return True diff --git a/monitorstack/plugins/os_vm_quota_cores.py b/monitorstack/plugins/os_vm_quota_cores.py new file mode 100644 index 0000000..37198e0 --- /dev/null +++ b/monitorstack/plugins/os_vm_quota_cores.py @@ -0,0 +1,59 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova cores quotas.""" +COMMAND_NAME = 'os_vm_quota_cores' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'quotas': 'cores' + }, + 'variables': {} + } + nova_config = utils.read_config(config_file=config_file)['nova'] + interface = nova_config.pop('interface', 'internal') + _ost = ost.OpenStack(os_auth_args=nova_config) + try: + variables = output['variables'] + for project in _ost.get_projects(): + limits = _ost.get_compute_limits( + project_id=project.id, + interface=interface + ) + variables[project.name] = int(limits['quota_set']['cores']) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_quota_instance.py b/monitorstack/plugins/os_vm_quota_instance.py new file mode 100644 index 0000000..b864d5e --- /dev/null +++ b/monitorstack/plugins/os_vm_quota_instance.py @@ -0,0 +1,59 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova instance quotas.""" +COMMAND_NAME = 'os_vm_quota_instance' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'quotas': 'instances' + }, + 'variables': {} + } + nova_config = utils.read_config(config_file=config_file)['nova'] + interface = nova_config.pop('interface', 'internal') + _ost = ost.OpenStack(os_auth_args=nova_config) + try: + variables = output['variables'] + for project in _ost.get_projects(): + limits = _ost.get_compute_limits( + project_id=project.id, + interface=interface + ) + variables[project.name] = int(limits['quota_set']['instances']) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_quota_ram.py b/monitorstack/plugins/os_vm_quota_ram.py new file mode 100644 index 0000000..556ea14 --- /dev/null +++ b/monitorstack/plugins/os_vm_quota_ram.py @@ -0,0 +1,59 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova ram quotas.""" +COMMAND_NAME = 'os_vm_quota_ram' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'quotas': 'ram' + }, + 'variables': {} + } + nova_config = utils.read_config(config_file=config_file)['nova'] + interface = nova_config.pop('interface', 'internal') + _ost = ost.OpenStack(os_auth_args=nova_config) + try: + variables = output['variables'] + for project in _ost.get_projects(): + limits = _ost.get_compute_limits( + project_id=project.id, + interface=interface + ) + variables[project.name] = int(limits['quota_set']['ram']) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_used_cores.py b/monitorstack/plugins/os_vm_used_cores.py new file mode 100644 index 0000000..955558c --- /dev/null +++ b/monitorstack/plugins/os_vm_used_cores.py @@ -0,0 +1,63 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import collections + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova used cores.""" +COMMAND_NAME = 'os_vm_used_cores' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'used': 'cores' + }, + 'variables': {} + } + + used_collection = collections.Counter() + nova_config = utils.read_config(config_file=config_file)['nova'] + _ost = ost.OpenStack(os_auth_args=nova_config) + flavors = _ost.get_flavors() + try: + variables = output['variables'] + for used in _ost.get_consumer_usage(): + flavor = flavors[used['flavor']['id']] + used_collection[used['name']] += int(flavor['vcpus']) + output['meta'][used['flavor']['id']] = True + output['meta'][used['flavor']['name']] = True + variables.update(used_collection) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_used_disk.py b/monitorstack/plugins/os_vm_used_disk.py new file mode 100644 index 0000000..b91435e --- /dev/null +++ b/monitorstack/plugins/os_vm_used_disk.py @@ -0,0 +1,63 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import collections + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova used disk.""" +COMMAND_NAME = 'os_vm_used_disk' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'used': 'disk' + }, + 'variables': {} + } + + used_collection = collections.Counter() + nova_config = utils.read_config(config_file=config_file)['nova'] + _ost = ost.OpenStack(os_auth_args=nova_config) + flavors = _ost.get_flavors() + try: + variables = output['variables'] + for used in _ost.get_consumer_usage(): + flavor = flavors[used['flavor']['id']] + used_collection[used['name']] += int(flavor['disk']) + output['meta'][used['flavor']['id']] = True + output['meta'][used['flavor']['name']] = True + variables.update(used_collection) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_used_instance.py b/monitorstack/plugins/os_vm_used_instance.py new file mode 100644 index 0000000..53ef0b9 --- /dev/null +++ b/monitorstack/plugins/os_vm_used_instance.py @@ -0,0 +1,59 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import collections + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova used instances.""" +COMMAND_NAME = 'os_vm_used_instance' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'used': 'instances' + }, + 'variables': {} + } + + used_collection = collections.Counter() + nova_config = utils.read_config(config_file=config_file)['nova'] + _ost = ost.OpenStack(os_auth_args=nova_config) + try: + variables = output['variables'] + for used in _ost.get_consumer_usage(): + used_collection[used['name']] += 1 + variables.update(used_collection) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/os_vm_used_ram.py b/monitorstack/plugins/os_vm_used_ram.py new file mode 100644 index 0000000..461d4ed --- /dev/null +++ b/monitorstack/plugins/os_vm_used_ram.py @@ -0,0 +1,63 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import collections + +import click + +from monitorstack import utils +from monitorstack.cli import pass_context +from monitorstack.utils import os_utils as ost + + +DOC = """Get nova used ram.""" +COMMAND_NAME = 'os_vm_used_ram' + + +@click.command(COMMAND_NAME, short_help=DOC) +@click.option('--config-file', + help='OpenStack configuration file', + default='openstack.ini') +@pass_context +def cli(ctx, config_file): + setattr(cli, '__doc__', DOC) + + output = { + 'measurement_name': COMMAND_NAME, + 'meta': { + 'used': 'ram' + }, + 'variables': {} + } + + used_collection = collections.Counter() + nova_config = utils.read_config(config_file=config_file)['nova'] + _ost = ost.OpenStack(os_auth_args=nova_config) + flavors = _ost.get_flavors() + try: + variables = output['variables'] + for used in _ost.get_consumer_usage(): + flavor = flavors[used['flavor']['id']] + used_collection[used['name']] += int(flavor['ram']) + output['meta'][used['flavor']['id']] = True + output['meta'][used['flavor']['name']] = True + variables.update(used_collection) + except Exception as exp: + output['exit_code'] = 1 + output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp) + else: + output['exit_code'] = 0 + output['message'] = '{} is ok'.format(COMMAND_NAME) + finally: + return output diff --git a/monitorstack/plugins/uptime.py b/monitorstack/plugins/uptime.py index 4cc94a3..9d9f0d0 100644 --- a/monitorstack/plugins/uptime.py +++ b/monitorstack/plugins/uptime.py @@ -24,6 +24,7 @@ from monitorstack.cli import pass_context @pass_context def cli(ctx): """Get system uptime.""" + uptime = get_uptime() output = { 'exit_code': 0, @@ -41,6 +42,7 @@ def cli(ctx): def get_uptime(): """Read the uptime from the proc filesystem.""" + with open('/proc/uptime', 'r') as f: output = f.read() diff --git a/monitorstack/utils/__init__.py b/monitorstack/utils/__init__.py new file mode 100644 index 0000000..523149a --- /dev/null +++ b/monitorstack/utils/__init__.py @@ -0,0 +1,95 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +import os +import shelve +import sys +import tempfile + +# Lower import to support conditional configuration parser +try: + if sys.version_info > (3, 2, 0): + import configparser as ConfigParser + else: + import ConfigParser +except ImportError: + raise SystemExit('No configparser module was found.') + + +def is_int(value): + for v_type in [int, float]: + try: + value = v_type(value) + except ValueError: + pass # v was not a int, float, or long + else: + return value + else: + return value + + +class LocalCache(object): + """Context Manager for opening and closing access to the DBM.""" + + def __init__(self): + """Set the Path to the DBM to create/Open.""" + + self.db_cache = os.path.join( + tempfile.gettempdir(), + 'monitorstack.openstack.dbm' + ) + + def __enter__(self): + """Open the DBM in r/w mode. + + :return: Open DBM + """ + + return self.open_shelve + + def __exit__(self, type, value, traceback): + """Close DBM Connection.""" + + self.close_shelve() + + def _open_shelve(self): + return shelve.open(self.db_cache) + + @property + def open_shelve(self): + return self._open_shelve() + + def close_shelve(self): + self.open_shelve.close() + + +def read_config(config_file): + cfg = os.path.abspath(os.path.expanduser(config_file)) + if not os.path.isfile(cfg): + raise IOError('Config file "{}" was not found'.format(cfg)) + + parser = ConfigParser.ConfigParser() + parser.optionxform = str + parser.read([cfg]) + args = dict() + defaults = dict([(k, v) for k, v in parser.items(section='DEFAULT')]) + for section in parser.sections(): + if section == 'DEFAULT': + continue + + sec = args[section] = defaults + for key, value in parser.items(section): + sec[key] = is_int(value=value) + + return args diff --git a/monitorstack/utils/os_utils.py b/monitorstack/utils/os_utils.py new file mode 100644 index 0000000..7ad2464 --- /dev/null +++ b/monitorstack/utils/os_utils.py @@ -0,0 +1,90 @@ +# Copyright 2017, Kevin Carter +# +# 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. + +try: + from openstack import connection as os_conn + from openstack import exceptions as os_exp +except ImportError as e: + raise SystemExit('OpenStack plugins require access to the OpenStackSDK.' + ' Please install "python-openstacksdk".' + ' ERROR: %s' % str(e)) + +from monitorstack import utils + + +class OpenStack(object): + def __init__(self, os_auth_args): + self.os_auth_args = os_auth_args + + @property + def conn(self): + return os_conn.Connection(**self.os_auth_args) + + def get_consumer_usage(self, servers=None, marker=None, limit=512): + tenant_kwargs = {'details': True, 'all_tenants': True, 'limit': limit} + if not servers: + servers = list() + + if marker: + tenant_kwargs['marker'] = marker + + count = 0 + for server in self.conn.compute.servers(**tenant_kwargs): + servers.append(server) + count += 1 + + if count == limit: + return self.get_consumer_usage( + servers=servers, + marker=servers[-1].id + ) + + return servers + + def get_compute_limits(self, project_id, interface='internal'): + url = self.conn.compute.session.get_endpoint( + interface=interface, + service_type='compute' + ) + quota_data = self.conn.compute.session.get( + url + '/os-quota-sets/' + project_id + ) + return quota_data.json() + + def get_project_name(self, project_id): + with utils.LocalCache() as c: + try: + project_name = c.get(project_id) + if not project_name: + project_info = self.conn.identity.get_project(project_id) + project_name = c[project_info.id] = project_info.name + except os_exp.ResourceNotFound: + return None + else: + return project_name + + def get_projects(self): + _consumers = list() + with utils.LocalCache() as c: + for project in self.conn.identity.projects(): + _consumers.append(project) + c[project.id] = project.name + return _consumers + + def get_flavors(self): + flavor_cache = dict() + for flavor in self.conn.compute.flavors(): + entry = flavor_cache[flavor['id']] = dict() + entry.update(flavor) + return flavor_cache diff --git a/test-requirements.txt b/test-requirements.txt index 5bbae8f..4ef3f7e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,3 @@ pytest tox +mock>=2.0.0 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 5fb2c41..2962436 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -35,7 +35,7 @@ class TestFormatters(object): def test_current_time(self): """Test current_time().""" - result = formatters.current_time() + result = formatters._current_time() assert isinstance(result, int) assert result > 0 diff --git a/tests/test_os_vm.py b/tests/test_os_vm.py new file mode 100644 index 0000000..edd308f --- /dev/null +++ b/tests/test_os_vm.py @@ -0,0 +1,62 @@ +# Copyright 2017, Kevin Carter +# +# 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. +"""Tests for the KVM plugin.""" + +import json + +from click.testing import CliRunner + +from monitorstack.cli import cli + + +def _runner(module): + runner = CliRunner() + result = runner.invoke(cli, ['-f', 'json', module]) + return json.loads(result.output) + + +class TestOs(object): + """Tests for the os_vm.* monitors.""" + + def test_os_vm_quota_cores(self): + """Ensure the run() method works.""" + + # result_json = _runner(module='os_vm_quota_cores') + # variables = result_json['variables'] + # meta = result_json['meta'] + pass + + def test_os_vm_quota_instances(self): + """Ensure the run() method works.""" + pass + + def test_os_vm_quota_ram(self): + """Ensure the run() method works.""" + pass + + def test_os_vm_used_cores(self): + """Ensure the run() method works.""" + pass + + def test_os_vm_used_disk(self): + """Ensure the run() method works.""" + pass + + def test_os_vm_used_instances(self): + """Ensure the run() method works.""" + pass + + def test_os_vm_used_ram(self): + """Ensure the run() method works.""" + pass diff --git a/tests/test_plugin_kvm.py b/tests/test_plugin_kvm.py index 33e13e4..23d7667 100644 --- a/tests/test_plugin_kvm.py +++ b/tests/test_plugin_kvm.py @@ -46,7 +46,7 @@ class LibvirtStub(object): class TestKvm(object): - """Tests for the uptime monitor class.""" + """Tests for the kvm monitor.""" def test_run(self): """Ensure the run() method works.""" diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..7f60136 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,70 @@ +# Copyright 2017, Major Hayden +# +# 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. +"""Tests for the uptime plugin.""" + +import os +import tempfile +import unittest + +from monitorstack import utils + + +class TestUptime(unittest.TestCase): + """Tests for the utilities.""" + + def setUp(self): + os_config_file = os.path.expanduser( + os.path.abspath(__file__ + '/../../etc/openstack.ini') + ) + self.config = utils.read_config(os_config_file) + conf = utils.ConfigParser.RawConfigParser() + conf.read([os_config_file]) + self.config_defaults = conf.defaults() + + def tearDown(self): + local_cache = os.path.join( + tempfile.gettempdir(), + 'monitorstack.openstack.dbm' + ) + if os.path.exists(local_cache): + os.remove(local_cache) + + def test_is_int_is_int(self): + self.assertTrue(isinstance(utils.is_int(value=1), int)) + + def test_is_int_is_int_str(self): + self.assertTrue(isinstance(utils.is_int(value='1'), int)) + + def test_is_int_is_not_int(self): + self.assertTrue(isinstance(utils.is_int(value='a'), str)) + + def test_read_config_not_found(self): + self.assertRaises( + IOError, + utils.read_config, + 'not-a-file' + ) + + def test_read_config_found_dict_return(self): + self.assertTrue(isinstance(self.config, dict)) + + def test_read_config_found_defaults_in_sections(self): + for k, v in self.config.items(): + for key in self.config_defaults.keys(): + self.assertTrue(key in v.keys()) + + def test_local_cache(self): + with utils.LocalCache() as c: + c['test_key'] = True + self.assertTrue('test_key' in c)