diff --git a/os_faults/api/cloud_management.py b/os_faults/api/cloud_management.py index 68a221d..b74b396 100644 --- a/os_faults/api/cloud_management.py +++ b/os_faults/api/cloud_management.py @@ -16,10 +16,15 @@ import abc import six from os_faults.api import base_driver +from os_faults.api import error @six.add_metaclass(abc.ABCMeta) class CloudManagement(base_driver.BaseDriver): + SERVICE_NAME_TO_CLASS = {} + SUPPORTED_SERVICES = [] + SUPPORTED_NETWORKS = [] + def __init__(self): self.power_management = None @@ -31,7 +36,6 @@ class CloudManagement(base_driver.BaseDriver): """Verify connection to the cloud. """ - pass @abc.abstractmethod def get_nodes(self, fqdns=None): @@ -42,16 +46,31 @@ class CloudManagement(base_driver.BaseDriver): :param fqdns list of FQDNs or None to retrieve all nodes :return: NodesCollection """ - pass - @abc.abstractmethod def get_service(self, name): """Get service with specified name :param name: name of the serives :return: Service """ - pass + if name in self.SERVICE_NAME_TO_CLASS: + klazz = self.SERVICE_NAME_TO_CLASS[name] + return klazz(node_cls=self.NODE_CLS, + cloud_management=self, + power_management=self.power_management) + raise error.ServiceError( + '{} driver does not support {!r} service'.format( + self.NAME.title(), name)) + + @abc.abstractmethod + def execute_on_cloud(self, hosts, task, raise_on_error=True): + """Execute task on specified hosts within the cloud. + + :param hosts: List of host FQDNs + :param task: Ansible task + :param raise_on_error: throw exception in case of error + :return: Ansible execution result (list of records) + """ @classmethod def list_supported_services(cls): diff --git a/os_faults/api/node_collection.py b/os_faults/api/node_collection.py index 4a6f535..6b7389a 100644 --- a/os_faults/api/node_collection.py +++ b/os_faults/api/node_collection.py @@ -11,24 +11,53 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc - -import six +import collections +import logging +import random +from os_faults.api import error from os_faults.api.util import public +Host = collections.namedtuple('Host', ['ip', 'mac', 'fqdn']) + -@six.add_metaclass(abc.ABCMeta) class NodeCollection(object): - @abc.abstractmethod - def pick(self): + def __init__(self, cloud_management=None, power_management=None, + hosts=None): + self.cloud_management = cloud_management + self.power_management = power_management + self.hosts = hosts + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self.hosts)) + + def __len__(self): + return len(self.hosts) + + def get_ips(self): + return [host.ip for host in self.hosts] + + def get_macs(self): + return [host.mac for host in self.hosts] + + def iterate_hosts(self): + for host in self.hosts: + yield host + + def pick(self, count=1): """Pick one Node out of collection :return: NodeCollection consisting just one node """ + if count > len(self.hosts): + msg = 'Cannot pick {} from {} node(s)'.format( + count, len(self.hosts)) + raise error.NodeCollectionError(msg) + return self.__class__(cloud_management=self.cloud_management, + power_management=self.power_management, + hosts=random.sample(self.hosts, count)) - @abc.abstractmethod def run_task(self, task, raise_on_error=True): """Run ansible task on node colection @@ -36,13 +65,18 @@ class NodeCollection(object): :param raise_on_error: throw exception in case of error :return: AnsibleExecutionRecord with results of task """ + logging.info('Run task: %s on nodes: %s', task, self) + return self.cloud_management.execute_on_cloud( + self.get_ips(), task, raise_on_error=raise_on_error) @public def reboot(self): """Reboot all nodes gracefully """ - raise NotImplementedError + logging.info('Reboot nodes: %s', self) + task = {'command': 'reboot now'} + self.cloud_management.execute_on_cloud(self.get_ips(), task) @public def oom(self): @@ -56,21 +90,24 @@ class NodeCollection(object): """Power off all nodes abruptly """ - raise NotImplementedError + logging.info('Power off nodes: %s', self) + self.power_management.poweroff(self.get_macs()) @public def poweron(self): """Power on all nodes abruptly """ - raise NotImplementedError + logging.info('Power on nodes: %s', self) + self.power_management.poweron(self.get_macs()) @public def reset(self): """Reset (cold restart) all nodes """ - raise NotImplementedError + logging.info('Reset nodes: %s', self) + self.power_management.reset(self.get_macs()) @public def disconnect(self, network_name): diff --git a/os_faults/api/service.py b/os_faults/api/service.py index 180df72..a92080c 100644 --- a/os_faults/api/service.py +++ b/os_faults/api/service.py @@ -27,7 +27,6 @@ class Service(object): :return: NodesCollection """ - pass @public def restart(self, nodes=None): diff --git a/os_faults/common/__init__.py b/os_faults/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/os_faults/common/service.py b/os_faults/common/service.py new file mode 100644 index 0000000..e78da4b --- /dev/null +++ b/os_faults/common/service.py @@ -0,0 +1,111 @@ +# 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 logging +import signal + +from os_faults.ansible import executor +from os_faults.api import error +from os_faults.api import service +from os_faults import utils + + +class ServiceAsProcess(service.Service): + + def __init__(self, node_cls, cloud_management=None, power_management=None): + self.node_cls = node_cls + self.cloud_management = cloud_management + self.power_management = power_management + + def _run_task(self, task, nodes): + ips = nodes.get_ips() + if not ips: + raise error.ServiceError('Node collection is empty') + + results = self.cloud_management.execute_on_cloud(ips, task) + err = False + for result in results: + if result.status != executor.STATUS_OK: + logging.error( + 'Task {} failed on node {}'.format(task, result.host)) + err = True + if err: + raise error.ServiceError('Task failed on some nodes') + return results + + def get_nodes(self): + nodes = self.cloud_management.get_nodes() + ips = nodes.get_ips() + cmd = 'bash -c "ps ax | grep \'{}\'"'.format(self.GREP) + results = self.cloud_management.execute_on_cloud( + ips, {'command': cmd}, False) + success_ips = [r.host for r in results + if r.status == executor.STATUS_OK] + hosts = [h for h in nodes.hosts if h.ip in success_ips] + return self.node_cls(cloud_management=self.cloud_management, + power_management=self.power_management, + hosts=hosts) + + @utils.require_variables('RESTART_CMD', 'SERVICE_NAME') + def restart(self, nodes=None): + nodes = nodes if nodes is not None else self.get_nodes() + logging.info("Restart '%s' service on nodes: %s", self.SERVICE_NAME, + nodes.get_ips()) + self._run_task({'command': self.RESTART_CMD}, nodes) + + @utils.require_variables('GREP', 'SERVICE_NAME') + def kill(self, nodes=None): + nodes = nodes if nodes is not None else self.get_nodes() + logging.info("Kill '%s' service on nodes: %s", self.SERVICE_NAME, + nodes.get_ips()) + cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGKILL}} + self._run_task(cmd, nodes) + + @utils.require_variables('GREP', 'SERVICE_NAME') + def freeze(self, nodes=None, sec=None): + nodes = nodes if nodes is not None else self.get_nodes() + if sec: + cmd = {'freeze': {'grep': self.GREP, 'sec': sec}} + else: + cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGSTOP}} + logging.info("Freeze '%s' service %son nodes: %s", self.SERVICE_NAME, + ('for %s sec ' % sec) if sec else '', nodes.get_ips()) + self._run_task(cmd, nodes) + + @utils.require_variables('GREP', 'SERVICE_NAME') + def unfreeze(self, nodes=None): + nodes = nodes if nodes is not None else self.get_nodes() + logging.info("Unfreeze '%s' service on nodes: %s", self.SERVICE_NAME, + nodes.get_ips()) + cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGCONT}} + self._run_task(cmd, nodes) + + @utils.require_variables('PORT', 'SERVICE_NAME') + def plug(self, nodes=None): + nodes = nodes if nodes is not None else self.get_nodes() + logging.info("Open port %d for '%s' service on nodes: %s", + self.PORT[1], self.SERVICE_NAME, nodes.get_ips()) + self._run_task({'iptables': {'protocol': self.PORT[0], + 'port': self.PORT[1], + 'action': 'unblock', + 'service': self.SERVICE_NAME}}, nodes) + + @utils.require_variables('PORT', 'SERVICE_NAME') + def unplug(self, nodes=None): + nodes = nodes if nodes is not None else self.get_nodes() + logging.info("Close port %d for '%s' service on nodes: %s", + self.PORT[1], self.SERVICE_NAME, nodes.get_ips()) + self._run_task({'iptables': {'protocol': self.PORT[0], + 'port': self.PORT[1], + 'action': 'block', + 'service': self.SERVICE_NAME}}, nodes) diff --git a/os_faults/drivers/devstack.py b/os_faults/drivers/devstack.py index 312e9b6..7a5e549 100644 --- a/os_faults/drivers/devstack.py +++ b/os_faults/drivers/devstack.py @@ -11,59 +11,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc -from collections import namedtuple import logging -import six - from os_faults.ansible import executor from os_faults.api import cloud_management from os_faults.api import node_collection -from os_faults.api import service -from os_faults import utils - -HostClass = namedtuple('HostClass', ['ip', 'mac']) +from os_faults.common import service class DevStackNode(node_collection.NodeCollection): - def __init__(self, cloud_management=None, power_management=None, - host=None): - self.cloud_management = cloud_management - self.power_management = power_management - self.host = host - - def __repr__(self): - return ('DevStackNode(%s)' % - dict(ip=self.host.ip, mac=self.host.mac)) - - def __len__(self): - return 1 - - def pick(self): - return self - - def run_task(self, task, raise_on_error=True): - # TODO(astudenov): refactor DevStackManagement.execute - # to be consistent with api - return self.cloud_management.execute(self.host.ip, task) - - def reboot(self): - task = {'command': 'reboot'} - self.cloud_management.execute(self.host.ip, task) - - def oom(self): - raise NotImplementedError - - def poweroff(self): - self.power_management.poweroff([self.host.mac]) - - def poweron(self): - self.power_management.poweron([self.host.mac]) - - def reset(self): - logging.info('Reset nodes: %s', self) - self.power_management.reset([self.host.mac]) def connect(self, network_name): raise NotImplementedError @@ -72,39 +28,19 @@ class DevStackNode(node_collection.NodeCollection): raise NotImplementedError -@six.add_metaclass(abc.ABCMeta) -class DevStackService(service.Service): - - def __init__(self, cloud_management=None, power_management=None): - self.cloud_management = cloud_management - self.power_management = power_management - - def __repr__(self): - return str(type(self)) - - def get_nodes(self): - return self.cloud_management.get_nodes() - - @utils.require_variables('RESTART_CMD', 'SERVICE_NAME') - def restart(self, nodes=None): - task = {'command': self.RESTART_CMD} - exec_res = self.cloud_management.execute(task) - logging.info('Restart %s result: %s', self.SERVICE_NAME, exec_res) - - -class KeystoneService(DevStackService): +class KeystoneService(service.ServiceAsProcess): SERVICE_NAME = 'keystone' + GREP = '[k]eystone-' RESTART_CMD = 'service apache2 restart' -SERVICE_NAME_TO_CLASS = { - 'keystone': KeystoneService, -} - - class DevStackManagement(cloud_management.CloudManagement): NAME = 'devstack' DESCRIPTION = 'Single node DevStack management driver' + NODE_CLS = DevStackNode + SERVICE_NAME_TO_CLASS = { + 'keystone': KeystoneService, + } SUPPORTED_SERVICES = list(SERVICE_NAME_TO_CLASS.keys()) SUPPORTED_NETWORKS = ['all-in-one'] CONFIG_SCHEMA = { @@ -127,7 +63,7 @@ class DevStackManagement(cloud_management.CloudManagement): self.username = cloud_management_params['username'] self.private_key_file = cloud_management_params.get('private_key_file') - self.executor = executor.AnsibleRunner( + self.cloud_executor = executor.AnsibleRunner( remote_user=self.username, private_key_file=self.private_key_file, become=True) self.host = None @@ -135,25 +71,33 @@ class DevStackManagement(cloud_management.CloudManagement): def verify(self): """Verify connection to the cloud.""" task = {'command': 'hostname'} - hostname = self.execute(task)[0].payload['stdout'] + hostname = self.execute_on_cloud( + [self.address], task)[0].payload['stdout'] logging.debug('DevStack hostname: %s', hostname) logging.info('Connected to cloud successfully') - def execute(self, task): - return self.executor.execute([self.address], task) + def execute_on_cloud(self, hosts, task, raise_on_error=True): + """Execute task on specified hosts within the cloud. + + :param hosts: List of host FQDNs + :param task: Ansible task + :param raise_on_error: throw exception in case of error + :return: Ansible execution result (list of records) + """ + if raise_on_error: + return self.cloud_executor.execute(hosts, task) + else: + return self.cloud_executor.execute(hosts, task, []) def get_nodes(self, fqdns=None): - if not self.host: + if self.host is None: task = {'command': 'cat /sys/class/net/eth0/address'} - mac = self.execute(task)[0].payload['stdout'] - self.host = HostClass(ip=self.address, mac=mac) + mac = self.execute_on_cloud( + [self.address], task)[0].payload['stdout'] + # TODO(astudenov): support fqdn + self.host = node_collection.Host(ip=self.address, mac=mac, + fqdn='') - return DevStackNode(cloud_management=self, - power_management=self.power_management, - host=self.host) - - def get_service(self, name): - if name in SERVICE_NAME_TO_CLASS: - klazz = SERVICE_NAME_TO_CLASS[name] - return klazz(cloud_management=self, - power_management=self.power_management) + return self.NODE_CLS(cloud_management=self, + power_management=self.power_management, + hosts=[self.host]) diff --git a/os_faults/drivers/fuel.py b/os_faults/drivers/fuel.py index 4394e45..2a9fbd6 100644 --- a/os_faults/drivers/fuel.py +++ b/os_faults/drivers/fuel.py @@ -11,79 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc import json import logging -import random -import signal - -import six from os_faults.ansible import executor from os_faults.api import cloud_management from os_faults.api import error from os_faults.api import node_collection -from os_faults.api import service -from os_faults import utils +from os_faults.common import service class FuelNodeCollection(node_collection.NodeCollection): - def __init__(self, cloud_management=None, power_management=None, - hosts=None): - self.cloud_management = cloud_management - self.power_management = power_management - self.hosts = hosts - - def __repr__(self): - return ('FuelNodeCollection(%s)' % - [dict(ip=h['ip'], mac=h['mac']) for h in self.hosts]) - - def __len__(self): - return len(self.hosts) - - def get_ips(self): - return [n['ip'] for n in self.hosts] - - def get_macs(self): - return [n['mac'] for n in self.hosts] - - def iterate_hosts(self): - for host in self.hosts: - yield host - - def pick(self, count=1): - if count > len(self.hosts): - msg = 'Cannot pick {} from {} node(s)'.format( - count, len(self.hosts)) - raise error.NodeCollectionError(msg) - return FuelNodeCollection(cloud_management=self.cloud_management, - power_management=self.power_management, - hosts=random.sample(self.hosts, count)) - - def run_task(self, task, raise_on_error=True): - logging.info('Run task: %s on nodes: %s', task, self) - return self.cloud_management.execute_on_cloud( - self.get_ips(), task, raise_on_error=raise_on_error) - - def reboot(self): - logging.info('Reboot nodes: %s', self) - task = {'command': 'reboot now'} - self.cloud_management.execute_on_cloud(self.get_ips(), task) - - def oom(self): - raise NotImplementedError - - def poweroff(self): - logging.info('Power off nodes: %s', self) - self.power_management.poweroff(self.get_macs()) - - def poweron(self): - logging.info('Power on nodes: %s', self) - self.power_management.poweron(self.get_macs()) - - def reset(self): - logging.info('Reset nodes: %s', self) - self.power_management.reset(self.get_macs()) def connect(self, network_name): logging.info("Connect network '%s' on nodes: %s", network_name, self) @@ -103,149 +41,55 @@ class FuelNodeCollection(node_collection.NodeCollection): self.cloud_management.execute_on_cloud(self.get_ips(), task) -@six.add_metaclass(abc.ABCMeta) -class FuelService(service.Service): - - def __init__(self, cloud_management=None, power_management=None): - self.cloud_management = cloud_management - self.power_management = power_management - - def __repr__(self): - return str(type(self)) - - def _run_task(self, task, nodes): - ips = nodes.get_ips() - if not ips: - raise error.ServiceError('Node collection is empty') - - results = self.cloud_management.execute_on_cloud(ips, task) - err = False - for result in results: - if result.status != executor.STATUS_OK: - logging.error( - 'Task {} failed on node {}'.format(task, result.host)) - err = True - if err: - raise error.ServiceError('Task failed on some nodes') - return results - - def get_nodes(self): - nodes = self.cloud_management.get_nodes() - ips = nodes.get_ips() - cmd = 'bash -c "ps ax | grep \'{}\'"'.format(self.GREP) - results = self.cloud_management.execute_on_cloud( - ips, {'command': cmd}, False) - success_ips = [r.host for r in results - if r.status == executor.STATUS_OK] - hosts = [h for h in nodes.hosts if h['ip'] in success_ips] - return FuelNodeCollection(cloud_management=self.cloud_management, - power_management=self.power_management, - hosts=hosts) - - @utils.require_variables('RESTART_CMD', 'SERVICE_NAME') - def restart(self, nodes=None): - nodes = nodes if nodes is not None else self.get_nodes() - logging.info("Restart '%s' service on nodes: %s", self.SERVICE_NAME, - nodes.get_ips()) - self._run_task({'command': self.RESTART_CMD}, nodes) - - @utils.require_variables('GREP', 'SERVICE_NAME') - def kill(self, nodes=None): - nodes = nodes if nodes is not None else self.get_nodes() - logging.info("Kill '%s' service on nodes: %s", self.SERVICE_NAME, - nodes.get_ips()) - cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGKILL}} - self._run_task(cmd, nodes) - - @utils.require_variables('GREP', 'SERVICE_NAME') - def freeze(self, nodes=None, sec=None): - nodes = nodes if nodes is not None else self.get_nodes() - if sec: - cmd = {'freeze': {'grep': self.GREP, 'sec': sec}} - else: - cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGSTOP}} - logging.info("Freeze '%s' service %son nodes: %s", self.SERVICE_NAME, - ('for %s sec ' % sec) if sec else '', nodes.get_ips()) - self._run_task(cmd, nodes) - - @utils.require_variables('GREP', 'SERVICE_NAME') - def unfreeze(self, nodes=None): - nodes = nodes if nodes is not None else self.get_nodes() - logging.info("Unfreeze '%s' service on nodes: %s", self.SERVICE_NAME, - nodes.get_ips()) - cmd = {'kill': {'grep': self.GREP, 'sig': signal.SIGCONT}} - self._run_task(cmd, nodes) - - @utils.require_variables('PORT', 'SERVICE_NAME') - def plug(self, nodes=None): - nodes = nodes if nodes is not None else self.get_nodes() - logging.info("Open port %d for '%s' service on nodes: %s", - self.PORT[1], self.SERVICE_NAME, nodes.get_ips()) - self._run_task({'iptables': {'protocol': self.PORT[0], - 'port': self.PORT[1], - 'action': 'unblock', - 'service': self.SERVICE_NAME}}, nodes) - - @utils.require_variables('PORT', 'SERVICE_NAME') - def unplug(self, nodes=None): - nodes = nodes if nodes is not None else self.get_nodes() - logging.info("Close port %d for '%s' service on nodes: %s", - self.PORT[1], self.SERVICE_NAME, nodes.get_ips()) - self._run_task({'iptables': {'protocol': self.PORT[0], - 'port': self.PORT[1], - 'action': 'block', - 'service': self.SERVICE_NAME}}, nodes) - - -class KeystoneService(FuelService): +class KeystoneService(service.ServiceAsProcess): SERVICE_NAME = 'keystone' GREP = '[k]eystone' RESTART_CMD = 'service apache2 restart' -class MemcachedService(FuelService): +class MemcachedService(service.ServiceAsProcess): SERVICE_NAME = 'memcached' GREP = '[m]emcached' RESTART_CMD = 'service memcached restart' -class MySQLService(FuelService): +class MySQLService(service.ServiceAsProcess): SERVICE_NAME = 'mysql' GREP = '[m]ysqld' PORT = ('tcp', 3307) -class RabbitMQService(FuelService): +class RabbitMQService(service.ServiceAsProcess): SERVICE_NAME = 'rabbitmq' GREP = '[r]abbit tcp_listeners' RESTART_CMD = 'service rabbitmq-server restart' -class NovaAPIService(FuelService): +class NovaAPIService(service.ServiceAsProcess): SERVICE_NAME = 'nova-api' GREP = '[n]ova-api' RESTART_CMD = 'service nova-api restart' -class GlanceAPIService(FuelService): +class GlanceAPIService(service.ServiceAsProcess): SERVICE_NAME = 'glance-api' GREP = '[g]lance-api' RESTART_CMD = 'service glance-api restart' -class NovaComputeService(FuelService): +class NovaComputeService(service.ServiceAsProcess): SERVICE_NAME = 'nova-compute' GREP = '[n]ova-compute' RESTART_CMD = 'service nova-compute restart' -class NovaSchedulerService(FuelService): +class NovaSchedulerService(service.ServiceAsProcess): SERVICE_NAME = 'nova-scheduler' GREP = '[n]ova-scheduler' RESTART_CMD = 'service nova-scheduler restart' -class NeutronOpenvswitchAgentService(FuelService): +class NeutronOpenvswitchAgentService(service.ServiceAsProcess): SERVICE_NAME = 'neutron-openvswitch-agent' GREP = '[n]eutron-openvswitch-agent' RESTART_CMD = ('bash -c "if pcs resource show neutron-openvswitch-agent; ' @@ -253,7 +97,7 @@ class NeutronOpenvswitchAgentService(FuelService): 'else service neutron-openvswitch-agent restart; fi"') -class NeutronL3AgentService(FuelService): +class NeutronL3AgentService(service.ServiceAsProcess): SERVICE_NAME = 'neutron-l3-agent' GREP = '[n]eutron-l3-agent' RESTART_CMD = ('bash -c "if pcs resource show neutron-l3-agent; ' @@ -261,37 +105,36 @@ class NeutronL3AgentService(FuelService): 'else service neutron-l3-agent restart; fi"') -class HeatAPIService(FuelService): +class HeatAPIService(service.ServiceAsProcess): SERVICE_NAME = 'heat-api' GREP = '[h]eat-api' RESTART_CMD = 'service heat-api restart' -class HeatEngineService(FuelService): +class HeatEngineService(service.ServiceAsProcess): SERVICE_NAME = 'heat-engine' GREP = '[h]eat-engine' RESTART_CMD = 'pcs resource restart p_heat-engine' -SERVICE_NAME_TO_CLASS = { - 'keystone': KeystoneService, - 'memcached': MemcachedService, - 'mysql': MySQLService, - 'rabbitmq': RabbitMQService, - 'nova-api': NovaAPIService, - 'glance-api': GlanceAPIService, - 'nova-compute': NovaComputeService, - 'nova-scheduler': NovaSchedulerService, - 'neutron-openvswitch-agent': NeutronOpenvswitchAgentService, - 'neutron-l3-agent': NeutronL3AgentService, - 'heat-api': HeatAPIService, - 'heat-engine': HeatEngineService, -} - - class FuelManagement(cloud_management.CloudManagement): NAME = 'fuel' DESCRIPTION = 'Fuel 9.x cloud management driver' + NODE_CLS = FuelNodeCollection + SERVICE_NAME_TO_CLASS = { + 'keystone': KeystoneService, + 'memcached': MemcachedService, + 'mysql': MySQLService, + 'rabbitmq': RabbitMQService, + 'nova-api': NovaAPIService, + 'glance-api': GlanceAPIService, + 'nova-compute': NovaComputeService, + 'nova-scheduler': NovaSchedulerService, + 'neutron-openvswitch-agent': NeutronOpenvswitchAgentService, + 'neutron-l3-agent': NeutronL3AgentService, + 'heat-api': HeatAPIService, + 'heat-engine': HeatEngineService, + } SUPPORTED_SERVICES = list(SERVICE_NAME_TO_CLASS.keys()) SUPPORTED_NETWORKS = ['management', 'private', 'public', 'storage'] CONFIG_SCHEMA = { @@ -330,7 +173,7 @@ class FuelManagement(cloud_management.CloudManagement): logging.debug('Cloud nodes: %s', hosts) task = {'command': 'hostname'} - host_addrs = [n['ip'] for n in hosts] + host_addrs = [host.ip for host in hosts] task_result = self.execute_on_cloud(host_addrs, task) logging.debug('Hostnames of cloud nodes: %s', [r.payload['stdout'] for r in task_result]) @@ -342,9 +185,10 @@ class FuelManagement(cloud_management.CloudManagement): task = {'command': 'fuel node --json'} result = self.execute_on_master_node(task) for r in json.loads(result[0].payload['stdout']): - host = {'ip': r['ip'], 'mac': r['mac'], 'fqdn': r['fqdn']} + host = node_collection.Host(ip=r['ip'], mac=r['mac'], + fqdn=r['fqdn']) self.cached_cloud_hosts.append(host) - self.fqdn_to_hosts[host['fqdn']] = host + self.fqdn_to_hosts[host.fqdn] = host return self.cached_cloud_hosts @@ -391,20 +235,6 @@ class FuelManagement(cloud_management.CloudManagement): 'Node with FQDN \'%s\' not found!' % fqdn) logging.debug('The following nodes were found: %s', hosts) - return FuelNodeCollection(cloud_management=self, - power_management=self.power_management, - hosts=hosts) - - def get_service(self, name): - """Get service with specified name - - :param name: name of the service - :return: Service - """ - if name in SERVICE_NAME_TO_CLASS: - klazz = SERVICE_NAME_TO_CLASS[name] - return klazz(cloud_management=self, - power_management=self.power_management) - raise error.ServiceError( - '{} driver does not support {!r} service'.format( - self.NAME.title(), name)) + return self.NODE_CLS(cloud_management=self, + power_management=self.power_management, + hosts=hosts) diff --git a/os_faults/tests/unit/drivers/test_devstack.py b/os_faults/tests/unit/drivers/test_devstack.py index 78537f7..fa73839 100644 --- a/os_faults/tests/unit/drivers/test_devstack.py +++ b/os_faults/tests/unit/drivers/test_devstack.py @@ -16,6 +16,7 @@ import copy import ddt import mock +from os_faults.api import node_collection from os_faults.api import power_management from os_faults.drivers import devstack from os_faults.tests.unit import fakes @@ -30,30 +31,33 @@ class DevStackNodeTestCase(test.TestCase): spec=devstack.DevStackManagement) self.mock_power_management = mock.Mock( spec=power_management.PowerManagement) - self.host = devstack.HostClass(ip='10.0.0.2', mac='09:7b:74:90:63:c1') + self.host = node_collection.Host( + ip='10.0.0.2', mac='09:7b:74:90:63:c1', fqdn='') self.node_collection = devstack.DevStackNode( cloud_management=self.mock_cloud_management, power_management=self.mock_power_management, - host=copy.deepcopy(self.host)) + hosts=[copy.deepcopy(self.host)]) def test_len(self): self.assertEqual(1, len(self.node_collection)) def test_pick(self): - self.assertIs(self.node_collection, self.node_collection.pick()) + self.assertEqual(self.node_collection.hosts, + self.node_collection.pick().hosts) def test_run_task(self): result = self.node_collection.run_task({'foo': 'bar'}) - mock_execute = self.mock_cloud_management.execute - expected_result = mock_execute.return_value + mock_execute_on_cloud = self.mock_cloud_management.execute_on_cloud + expected_result = mock_execute_on_cloud.return_value self.assertIs(result, expected_result) - mock_execute.assert_called_once_with('10.0.0.2', {'foo': 'bar'}) + mock_execute_on_cloud.assert_called_once_with( + ['10.0.0.2'], {'foo': 'bar'}, raise_on_error=True) def test_reboot(self): self.node_collection.reboot() - self.mock_cloud_management.execute.assert_called_once_with( - '10.0.0.2', {'command': 'reboot'}) + self.mock_cloud_management.execute_on_cloud.assert_called_once_with( + ['10.0.0.2'], {'command': 'reboot now'}) def test_poweroff(self): self.node_collection.poweroff() @@ -91,13 +95,14 @@ class DevStackManagementTestCase(test.TestCase): ['10.0.0.2'], {'command': 'hostname'}) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - def test_execute(self, mock_ansible_runner): + def test_execute_on_cloud(self, mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value ansible_runner_inst.execute.side_effect = [ [fakes.FakeAnsibleResult(payload={'stdout': '/root'})], ] devstack_management = devstack.DevStackManagement(self.conf) - result = devstack_management.execute({'command': 'pwd'}) + result = devstack_management.execute_on_cloud( + ['10.0.0.2'], {'command': 'pwd'}) ansible_runner_inst.execute.assert_called_once_with( ['10.0.0.2'], {'command': 'pwd'}) @@ -119,8 +124,9 @@ class DevStackManagementTestCase(test.TestCase): self.assertIsInstance(nodes, devstack.DevStackNode) self.assertEqual( - devstack.HostClass(ip='10.0.0.2', mac='09:7b:74:90:63:c1'), - nodes.host) + [node_collection.Host(ip='10.0.0.2', mac='09:7b:74:90:63:c1', + fqdn='')], + nodes.hosts) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) @ddt.data(('keystone', devstack.KeystoneService)) @@ -129,7 +135,8 @@ class DevStackManagementTestCase(test.TestCase): mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value ansible_runner_inst.execute.side_effect = [ - [fakes.FakeAnsibleResult(payload={'stdout': '09:7b:74:90:63:c1'})] + [fakes.FakeAnsibleResult(payload={'stdout': '09:7b:74:90:63:c1'})], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')] ] devstack_management = devstack.DevStackManagement(self.conf) @@ -138,11 +145,17 @@ class DevStackManagementTestCase(test.TestCase): self.assertIsInstance(service, service_cls) nodes = service.get_nodes() - ansible_runner_inst.execute.assert_called_once_with( - ['10.0.0.2'], {'command': 'cat /sys/class/net/eth0/address'}) + + cmd = 'bash -c "ps ax | grep \'{}\'"'.format(service_cls.GREP) + ansible_runner_inst.execute.assert_has_calls([ + mock.call( + ['10.0.0.2'], {'command': 'cat /sys/class/net/eth0/address'}), + mock.call(['10.0.0.2'], {'command': cmd}, []) + ]) self.assertEqual( - devstack.HostClass(ip='10.0.0.2', mac='09:7b:74:90:63:c1'), - nodes.host) + [node_collection.Host(ip='10.0.0.2', mac='09:7b:74:90:63:c1', + fqdn='')], + nodes.hosts) @ddt.ddt @@ -159,6 +172,7 @@ class DevStackServiceTestCase(test.TestCase): ansible_runner_inst = mock_ansible_runner.return_value ansible_runner_inst.execute.side_effect = [ [fakes.FakeAnsibleResult(payload={'stdout': '09:7b:74:90:63:c1'})], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')], [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')] ] @@ -168,7 +182,11 @@ class DevStackServiceTestCase(test.TestCase): self.assertIsInstance(service, service_cls) service.restart() + + cmd = 'bash -c "ps ax | grep \'{}\'"'.format(service_cls.GREP) ansible_runner_inst.execute.assert_has_calls([ - mock.call(['10.0.0.2'], - {'command': service_cls.RESTART_CMD}), + mock.call( + ['10.0.0.2'], {'command': 'cat /sys/class/net/eth0/address'}), + mock.call(['10.0.0.2'], {'command': cmd}, []), + mock.call(['10.0.0.2'], {'command': service_cls.RESTART_CMD}) ]) diff --git a/os_faults/tests/unit/drivers/test_fuel_management.py b/os_faults/tests/unit/drivers/test_fuel_management.py index 99399ed..4748a68 100644 --- a/os_faults/tests/unit/drivers/test_fuel_management.py +++ b/os_faults/tests/unit/drivers/test_fuel_management.py @@ -16,6 +16,7 @@ import mock from os_faults.ansible import executor from os_faults.api import error +from os_faults.api import node_collection from os_faults.drivers import fuel from os_faults.tests.unit import fakes from os_faults.tests.unit import test @@ -66,9 +67,11 @@ class FuelManagementTestCase(test.TestCase): mock.call(['fuel.local'], {'command': 'fuel node --json'}), ]) - self.assertEqual(nodes.hosts, - [{'ip': '10.0.0.2', 'mac': '02', "fqdn": "node-2"}, - {'ip': '10.0.0.3', 'mac': '03', "fqdn": "node-3"}]) + hosts = [ + node_collection.Host(ip='10.0.0.2', mac='02', fqdn='node-2'), + node_collection.Host(ip='10.0.0.3', mac='03', fqdn='node-3'), + ] + self.assertEqual(nodes.hosts, hosts) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) def test_execute_on_cloud(self, mock_ansible_runner): @@ -105,11 +108,13 @@ class FuelManagementTestCase(test.TestCase): }) nodes = fuel_managment.get_nodes(fqdns=['node-3']) - self.assertEqual(nodes.hosts, - [{'ip': '10.0.0.3', 'mac': '03', 'fqdn': 'node-3'}]) + hosts = [ + node_collection.Host(ip='10.0.0.3', mac='03', fqdn='node-3'), + ] + self.assertEqual(nodes.hosts, hosts) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - @ddt.data(*fuel.SERVICE_NAME_TO_CLASS.items()) + @ddt.data(*fuel.FuelManagement.SERVICE_NAME_TO_CLASS.items()) @ddt.unpack def test_get_service_nodes(self, service_name, service_cls, mock_ansible_runner): @@ -138,8 +143,11 @@ class FuelManagementTestCase(test.TestCase): mock.call(['10.0.0.2', '10.0.0.3'], {'command': cmd}, []), ]) - self.assertEqual(nodes.hosts, - [{'ip': '10.0.0.3', 'mac': '03', "fqdn": "node-3"}]) + + hosts = [ + node_collection.Host(ip='10.0.0.3', mac='03', fqdn='node-3'), + ] + self.assertEqual(nodes.hosts, hosts) def test_get_unknown_service(self): fuel_managment = fuel.FuelManagement({ diff --git a/os_faults/tests/unit/drivers/test_fuel_node_collection.py b/os_faults/tests/unit/drivers/test_fuel_node_collection.py index 470f180..d7123f2 100644 --- a/os_faults/tests/unit/drivers/test_fuel_node_collection.py +++ b/os_faults/tests/unit/drivers/test_fuel_node_collection.py @@ -16,6 +16,7 @@ import copy import mock from os_faults.api import error +from os_faults.api import node_collection from os_faults.api import power_management from os_faults.drivers import fuel from os_faults.tests.unit import test @@ -29,10 +30,14 @@ class FuelNodeCollectionTestCase(test.TestCase): self.mock_power_management = mock.Mock( spec=power_management.PowerManagement) self.hosts = [ - dict(ip='10.0.0.2', mac='09:7b:74:90:63:c1', fqdn='node1.com'), - dict(ip='10.0.0.3', mac='09:7b:74:90:63:c2', fqdn='node2.com'), - dict(ip='10.0.0.4', mac='09:7b:74:90:63:c3', fqdn='node3.com'), - dict(ip='10.0.0.5', mac='09:7b:74:90:63:c4', fqdn='node4.com'), + node_collection.Host(ip='10.0.0.2', mac='09:7b:74:90:63:c1', + fqdn='node1.com'), + node_collection.Host(ip='10.0.0.3', mac='09:7b:74:90:63:c2', + fqdn='node2.com'), + node_collection.Host(ip='10.0.0.4', mac='09:7b:74:90:63:c3', + fqdn='node3.com'), + node_collection.Host(ip='10.0.0.5', mac='09:7b:74:90:63:c4', + fqdn='node4.com'), ] self.node_collection = fuel.FuelNodeCollection( diff --git a/os_faults/tests/unit/drivers/test_fuel_service.py b/os_faults/tests/unit/drivers/test_fuel_service.py index 35af92c..40f5c2b 100644 --- a/os_faults/tests/unit/drivers/test_fuel_service.py +++ b/os_faults/tests/unit/drivers/test_fuel_service.py @@ -34,7 +34,7 @@ class FuelServiceTestCase(test.TestCase): }) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - @ddt.data(*fuel.SERVICE_NAME_TO_CLASS.items()) + @ddt.data(*fuel.FuelManagement.SERVICE_NAME_TO_CLASS.items()) @ddt.unpack def test_kill(self, service_name, service_cls, mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value @@ -68,7 +68,7 @@ class FuelServiceTestCase(test.TestCase): ]) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - @ddt.data(*fuel.SERVICE_NAME_TO_CLASS.items()) + @ddt.data(*fuel.FuelManagement.SERVICE_NAME_TO_CLASS.items()) @ddt.unpack def test_freeze(self, service_name, service_cls, mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value @@ -102,7 +102,7 @@ class FuelServiceTestCase(test.TestCase): ]) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - @ddt.data(*fuel.SERVICE_NAME_TO_CLASS.items()) + @ddt.data(*fuel.FuelManagement.SERVICE_NAME_TO_CLASS.items()) @ddt.unpack def test_freeze_sec(self, service_name, service_cls, mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value @@ -138,7 +138,7 @@ class FuelServiceTestCase(test.TestCase): ]) @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) - @ddt.data(*fuel.SERVICE_NAME_TO_CLASS.items()) + @ddt.data(*fuel.FuelManagement.SERVICE_NAME_TO_CLASS.items()) @ddt.unpack def test_unfreeze(self, service_name, service_cls, mock_ansible_runner): ansible_runner_inst = mock_ansible_runner.return_value