diff --git a/os_failures/__init__.py b/os_failures/__init__.py index acc0a8d..152878c 100644 --- a/os_failures/__init__.py +++ b/os_failures/__init__.py @@ -14,6 +14,13 @@ import pbr.version +from os_failures.drivers import fuel __version__ = pbr.version.VersionInfo( 'os_failures').version_string() + + +def build_client(cloud_config): + cloud_management = cloud_config.get('cloud_management') or {} + if 'fuel' in cloud_management: + return fuel.FuelClient(cloud_management['fuel']) diff --git a/os_failures/ansible/__init__.py b/os_failures/ansible/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_failures/ansible/modules/__init__.py b/os_failures/ansible/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_failures/ansible/runner.py b/os_failures/ansible/runner.py new file mode 100644 index 0000000..f3112c2 --- /dev/null +++ b/os_failures/ansible/runner.py @@ -0,0 +1,156 @@ +# Copyright (c) 2016 OpenStack Foundation +# +# 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 collections import namedtuple +import copy + +from ansible.executor import task_queue_manager +from ansible import inventory +from ansible.parsing import dataloader +from ansible.playbook import play +from ansible.plugins import callback as callback_pkg +from ansible.vars import VariableManager +from oslo_log import log as logging + +from os_failures import utils + +LOG = logging.getLogger(__name__) + + +def _light_rec(result): + for r in result: + c = copy.deepcopy(r) + if 'records' in c: + del c['records'] + if 'series' in c: + del c['series'] + yield c + + +def _log_result(result): + # todo check current log level before doing heavy things + LOG.debug('Execution result (filtered): %s', list(_light_rec(result))) + + +class MyCallback(callback_pkg.CallbackBase): + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'myown' + + def __init__(self, storage, display=None): + super(MyCallback, self).__init__(display) + self.storage = storage + + def _store(self, result, status): + record = dict(host=result._host.get_name(), + status=status, + task=result._task.get_name(), + payload=result._result) + self.storage.append(record) + + def v2_runner_on_failed(self, result, ignore_errors=False): + super(MyCallback, self).v2_runner_on_failed(result) + self._store(result, 'FAILED') + + def v2_runner_on_ok(self, result): + super(MyCallback, self).v2_runner_on_ok(result) + self._store(result, 'OK') + + def v2_runner_on_skipped(self, result): + super(MyCallback, self).v2_runner_on_skipped(result) + self._store(result, 'SKIPPED') + + def v2_runner_on_unreachable(self, result): + super(MyCallback, self).v2_runner_on_unreachable(result) + self._store(result, 'UNREACHABLE') + + +Options = namedtuple('Options', + ['connection', 'password', 'module_path', 'forks', + 'remote_user', + 'private_key_file', 'ssh_common_args', 'ssh_extra_args', + 'sftp_extra_args', 'scp_extra_args', 'become', + 'become_method', 'become_user', 'verbosity', 'check']) + + +class AnsibleRunner(object): + def __init__(self, remote_user='root', password=None, forks=100, + ssh_common_args=None): + super(AnsibleRunner, self).__init__() + + module_path = utils.resolve_relative_path( + 'os_failures/ansible/modules') + self.options = Options( + connection='smart', password=password, module_path=module_path, + forks=forks, remote_user=remote_user, private_key_file=None, + ssh_common_args=ssh_common_args, ssh_extra_args=None, sftp_extra_args=None, + scp_extra_args=None, become=None, become_method='sudo', + become_user='root', verbosity=100, check=False) + + def _run_play(self, play_source): + LOG.debug('Running play: %s', play_source) + + host_list = play_source['hosts'] + + loader = dataloader.DataLoader() + variable_manager = VariableManager() + inventory_inst = inventory.Inventory(loader=loader, + variable_manager=variable_manager, + host_list=host_list) + variable_manager.set_inventory(inventory_inst) + passwords = dict(vault_pass='secret') + + # create play + play_inst = play.Play().load(play_source, + variable_manager=variable_manager, + loader=loader) + + storage = [] + callback = MyCallback(storage) + + # actually run it + tqm = None + try: + tqm = task_queue_manager.TaskQueueManager( + inventory=inventory_inst, + variable_manager=variable_manager, + loader=loader, + options=self.options, + passwords=passwords, + stdout_callback=callback, + ) + tqm.run(play_inst) + finally: + if tqm is not None: + tqm.cleanup() + + _log_result(storage) + + return storage + + def run(self, playbook): + result = [] + + for play_source in playbook: + play_source['gather_facts'] = 'no' + + result += self._run_play(play_source) + + return result + + def execute(self, hosts, task): + task_play = {'hosts': hosts, 'tasks': [task]} + return self.run([task_play]) diff --git a/os_failures/api/__init__.py b/os_failures/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_failures/api/client.py b/os_failures/api/client.py new file mode 100644 index 0000000..6d19576 --- /dev/null +++ b/os_failures/api/client.py @@ -0,0 +1,9 @@ +import abc + + +class Client(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def get_cloud(self): + pass diff --git a/os_failures/api/cloud.py b/os_failures/api/cloud.py new file mode 100644 index 0000000..750203d --- /dev/null +++ b/os_failures/api/cloud.py @@ -0,0 +1,9 @@ +import abc + + +class Cloud(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def get_nodes(self, role): + pass diff --git a/os_failures/api/node.py b/os_failures/api/node.py new file mode 100644 index 0000000..2249a54 --- /dev/null +++ b/os_failures/api/node.py @@ -0,0 +1,17 @@ +import abc + + +class Node(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def kill_process(self, group): + pass + + @abc.abstractmethod + def oom(self, group): + pass + + @abc.abstractmethod + def reboot(self, group): + pass diff --git a/os_failures/drivers/__init__.py b/os_failures/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_failures/drivers/fuel.py b/os_failures/drivers/fuel.py new file mode 100644 index 0000000..2679dee --- /dev/null +++ b/os_failures/drivers/fuel.py @@ -0,0 +1,40 @@ +from os_failures.ansible import runner +from os_failures.api import client +from os_failures.api import cloud + + +ROLE_MAPPING = { + 'keystone-api': 'controller', +} + + +class FuelCloud(cloud.Cloud): + + def __init__(self, client): + self.client = client + + def get_nodes(self, role): + fuel_role = ROLE_MAPPING[role] + + +class FuelClient(client.Client): + def __init__(self, params): + self.ip = params['ip'] + self.username = params['username'] + self.password = params['password'] + + self.ansible_executor = runner.AnsibleRunner( + remote_user=self.username) + + task = {'command': 'fuel2 node list -f json'} + nodes = self.ansible_executor.execute([self.ip], task) + print(nodes) + + self.ansible_executor = runner.AnsibleRunner( + remote_user=self.username, + ssh_common_args='-o ProxyCommand="ssh -W %h:%p root@172.18.171.149"') + task = {'command': 'hostname'} + print(self.ansible_executor.execute(['10.20.0.3', '10.20.0.4'], task)) + + def get_cloud(self): + return FuelCloud(self) diff --git a/os_failures/tests/sample.py b/os_failures/tests/sample.py new file mode 100644 index 0000000..48545f8 --- /dev/null +++ b/os_failures/tests/sample.py @@ -0,0 +1,38 @@ +# 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_failures + + +def main(): + cloud_config = { + 'auth': { + 'username': 'admin', + 'password': 'admin', + 'project_name': 'admin', + }, + 'region_name': 'RegionOne', + 'cloud_management': { + 'fuel': { + 'ip': '172.18.171.149', + 'username': 'root', + 'password': 'r00tme', + } + } + } + client = os_failures.build_client(cloud_config) + cloud = client.get_cloud() + keystone_nodes = cloud.get_nodes(role='keystone-api') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/os_failures/utils.py b/os_failures/utils.py new file mode 100644 index 0000000..6e65425 --- /dev/null +++ b/os_failures/utils.py @@ -0,0 +1,216 @@ +# Copyright (c) 2016 OpenStack Foundation +# +# 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 functools +import itertools +import logging as std_logging +import os +import random +import uuid + +from oslo_config import cfg +from oslo_log import log as logging +import re +import six +import yaml + + +LOG = logging.getLogger(__name__) + + +def env(*_vars, **kwargs): + """Returns the first environment variable set. + + If none are non-empty, defaults to '' or keyword arg default. + """ + for v in _vars: + value = os.environ.get(v) + if value: + return value + return kwargs.get('default', None) + + +def validate_required_opts(conf, opts): + # all config parameters default to ENV values, that's why standard + # check of required options doesn't work and needs to be done manually + for opt in opts: + if opt.required and not conf[opt.dest]: + raise cfg.RequiredOptError(opt.name) + + +def init_config_and_logging(opts): + conf = cfg.CONF + conf.register_cli_opts(opts) + conf.register_opts(opts) + logging.register_options(conf) + logging.set_defaults() + + try: + conf(project='performa') + validate_required_opts(conf, opts) + except cfg.RequiredOptError as e: + print('Error: %s' % e) + conf.print_usage() + exit(1) + + logging.setup(conf, 'performa') + LOG.info('Logging enabled') + conf.log_opt_values(LOG, std_logging.DEBUG) + + +def resolve_relative_path(file_name): + path = os.path.normpath(os.path.join( + os.path.dirname(__import__('os_failures').__file__), '../', file_name)) + if os.path.exists(path): + return path + + +def get_absolute_file_path(file_name, base_dir='', alias_mapper=None): + full_path = os.path.normpath(os.path.join(base_dir, file_name)) + + if alias_mapper: # interpret file_name as alias + alias_path = resolve_relative_path(alias_mapper(file_name)) + if alias_path: + full_path = alias_path + LOG.info('Alias "%s" is resolved into file "%s"', file_name, + full_path) + + if not os.path.exists(full_path): + # treat file_name as relative to act's package root + full_path = os.path.normpath( + os.path.join(os.path.dirname(__import__('performa').__file__), + '../', file_name)) + if not os.path.exists(full_path): + msg = ('File %s not found by absolute nor by relative path' % + file_name) + raise IOError(msg) + + return full_path + + +def read_file(file_name, base_dir='', alias_mapper=None): + full_path = get_absolute_file_path(file_name, base_dir, alias_mapper) + + fd = None + try: + fd = open(full_path) + return fd.read() + except IOError as e: + LOG.error('Error reading file: %s', e) + raise + finally: + if fd: + fd.close() + + +def write_file(data, file_name, base_dir=''): + full_path = os.path.normpath(os.path.join(base_dir, file_name)) + fd = None + try: + fd = open(full_path, 'w') + return fd.write(data) + except IOError as e: + LOG.error('Error writing file: %s', e) + raise + finally: + if fd: + fd.close() + + +def read_yaml_file(file_name, base_dir='', alias_mapper=None): + raw = read_file(file_name, base_dir, alias_mapper) + try: + parsed = yaml.safe_load(raw) + return parsed + except Exception as e: + LOG.error('Failed to parse file %(file)s in YAML format: %(err)s', + dict(file=file_name, err=e)) + + +def read_uri(uri): + try: + req = six.moves.urllib.request.Request(url=uri) + fd = six.moves.urllib.request.urlopen(req) + raw = fd.read() + fd.close() + return raw + except Exception as e: + LOG.warn('Error "%(error)s" while reading uri %(uri)s', + {'error': e, 'uri': uri}) + + +def random_string(length=6): + return ''.join(random.sample('adefikmoprstuz', length)) + + +def make_id(): + return str(uuid.uuid4()) + + +def make_help_options(message, base, type_filter=None): + path = resolve_relative_path(base) + files = itertools.chain.from_iterable( + [map(functools.partial(os.path.join, root), files) + for root, dirs, files in os.walk(path)]) # list of files in a tree + if type_filter: + files = (f for f in files if type_filter(f)) # filtered list + rel_files = map(functools.partial(os.path.relpath, start=path), files) + return message % ', '.join('"%s"' % f.partition('.')[0] + for f in sorted(rel_files)) + + +def algebraic_product(**kwargs): + position_to_key = {} + values = [] + total = 1 + + for key, item in six.iteritems(kwargs): + position_to_key[len(values)] = key + if type(item) != list: + item = [item] # enclose single item into the list + + values.append(item) + total *= len(item) + + LOG.debug('Total number of permutations is: %s', total) + + for chain in itertools.product(*values): + result = {} + for position, key in six.iteritems(position_to_key): + result[key] = chain[position] + yield result + + +def strict(s): + return re.sub(r'[^\w\d]+', '_', re.sub(r'\(.+\)', '', s)).lower() + + +def parse_url(url): + arr = url.split(':') + result = {} + + if len(arr) > 0: + result['host'] = arr[0] + + if len(arr) > 1: + if arr[1].isdigit(): + result['port'] = int(arr[1]) + else: + raise ValueError('URL should be in form of host[:port], ' + 'but got: %s', url) + else: + ValueError('URL should not be empty') + + return result diff --git a/requirements.txt b/requirements.txt index 30806d5..6be2327 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,10 @@ # process, which may cause wedges in the gate later. pbr>=1.6 + +ansible +iso8601>=0.1.9 +oslo.i18n>=1.5.0 # Apache-2.0 +oslo.log>=1.12.0 # Apache-2.0 +oslo.serialization>=1.10.0 # Apache-2.0 +oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0