diff --git a/doc/source/drivers.rst b/doc/source/drivers.rst index 706a2a1..63a65df 100644 --- a/doc/source/drivers.rst +++ b/doc/source/drivers.rst @@ -7,6 +7,8 @@ Cloud management .. cloud_driver_doc:: devstack +.. cloud_driver_doc:: devstack_systemd + .. cloud_driver_doc:: fuel .. cloud_driver_doc:: tcpcloud @@ -35,6 +37,8 @@ Service drivers .. driver_doc:: screen +.. driver_doc:: systemd_service + .. driver_doc:: salt_service .. driver_doc:: pcs_service diff --git a/os_faults/common/service.py b/os_faults/common/service.py index 5c3dc9a..5b03937 100644 --- a/os_faults/common/service.py +++ b/os_faults/common/service.py @@ -215,3 +215,55 @@ class LinuxService(ServiceAsProcess): self.restart_cmd = 'service {} restart'.format(self.linux_service) self.terminate_cmd = 'service {} stop'.format(self.linux_service) self.start_cmd = 'service {} start'.format(self.linux_service) + + +class SystemdService(ServiceAsProcess): + """Systemd service. + + Service as Systemd unit and can be controlled by `systemctl` CLI tool. + + **Example configuration:** + + .. code-block:: yaml + + services: + app: + driver: systemd_service + args: + systemd_service: app + grep: my_app + port: ['tcp', 4242] + + parameters: + + - **systemd_service** - name of a service in systemd + - **grep** - regexp for grep to find process PID + - **port** - tuple with two values - protocol, port number (optional) + + """ + NAME = 'systemd_service' + DESCRIPTION = 'Service in Systemd' + CONFIG_SCHEMA = { + 'type': 'object', + 'properties': { + 'systemd_service': {'type': 'string'}, + 'grep': {'type': 'string'}, + 'port': PORT_SCHEMA, + 'start_cmd': {'type': 'string'}, + 'terminate_cmd': {'type': 'string'}, + 'restart_cmd': {'type': 'string'}, + }, + 'required': ['grep', 'systemd_service'], + 'additionalProperties': False, + } + + def __init__(self, *args, **kwargs): + super(SystemdService, self).__init__(*args, **kwargs) + self.systemd_service = self.config['systemd_service'] + + self.restart_cmd = 'sudo systemctl restart {}'.format( + self.systemd_service) + self.terminate_cmd = 'sudo systemctl stop {}'.format( + self.systemd_service) + self.start_cmd = 'sudo systemctl start {}'.format( + self.systemd_service) diff --git a/os_faults/drivers/devstack_systemd.py b/os_faults/drivers/devstack_systemd.py new file mode 100644 index 0000000..2e270f1 --- /dev/null +++ b/os_faults/drivers/devstack_systemd.py @@ -0,0 +1,128 @@ +# 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 + +from os_faults.drivers import devstack + +LOG = logging.getLogger(__name__) + + +class DevStackSystemdManagement(devstack.DevStackManagement): + """Driver for modern DevStack based on Systemd. + + This driver requires DevStack installed with Systemd (USE_SCREEN=False). + Supports discovering of node MAC addresses. + + **Example configuration:** + + .. code-block:: yaml + + cloud_management: + driver: devstack_systemd + args: + address: 192.168.1.10 + username: ubuntu + password: ubuntu_pass + private_key_file: ~/.ssh/id_rsa_devstack_systemd + slaves: + - 192.168.1.11 + - 192.168.1.12 + iface: eth1 + + parameters: + + - **address** - ip address of any devstack node + - **username** - username for all nodes + - **password** - password for all nodes (optional) + - **private_key_file** - path to key file (optional) + - **slaves** - list of ips for additional nodes (optional) + - **iface** - network interface name to retrieve mac address (optional) + - **serial** - how many hosts Ansible should manage at a single time. + (optional) default: 10 + """ + + NAME = 'devstack_systemd' + DESCRIPTION = 'DevStack management driver using Systemd' + # NODE_CLS = DevStackNode + SERVICES = { + 'keystone': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'keystone-uwsgi', + 'systemd_service': 'devstack@keystone', + } + }, + 'mysql': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'mysqld', + 'systemd_service': 'mariadb', + 'port': ['tcp', 3307], + } + }, + 'rabbitmq': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'rabbitmq-server', + 'systemd_service': 'rabbit-server', + } + }, + 'nova-api': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'nova-api', + 'systemd_service': 'devstack@n-api', + } + }, + 'glance-api': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'glance-api', + 'systemd_service': 'devstack@g-api', + } + }, + 'nova-compute': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'n-cpu', + 'systemd_service': 'devstack@n-cpu', + } + }, + 'nova-scheduler': { + 'driver': 'systemd_service', + 'args': { + 'grep': 'nova-scheduler', + 'systemd_service': 'devstack@n-sch', + } + }, + } + SUPPORTED_NETWORKS = ['all-in-one'] + CONFIG_SCHEMA = { + 'type': 'object', + '$schema': 'http://json-schema.org/draft-04/schema#', + 'properties': { + 'address': {'type': 'string'}, + 'username': {'type': 'string'}, + 'password': {'type': 'string'}, + 'private_key_file': {'type': 'string'}, + 'slaves': { + 'type': 'array', + 'items': {'type': 'string'}, + }, + 'iface': {'type': 'string'}, + 'serial': {'type': 'integer', 'minimum': 1}, + }, + 'required': ['address', 'username'], + 'additionalProperties': False, + } diff --git a/os_faults/tests/unit/drivers/test_devstack_systemd.py b/os_faults/tests/unit/drivers/test_devstack_systemd.py new file mode 100644 index 0000000..857e99c --- /dev/null +++ b/os_faults/tests/unit/drivers/test_devstack_systemd.py @@ -0,0 +1,119 @@ +# 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 ddt +import mock + +from os_faults.api import node_collection +from os_faults.drivers import devstack_systemd +from os_faults.tests.unit.drivers import test_devstack +from os_faults.tests.unit import fakes +from os_faults.tests.unit import test + + +@ddt.ddt +class DevStackSystemdManagementTestCase( + test_devstack.DevStackManagementTestCase): + + def setUp(self): + super(DevStackSystemdManagementTestCase, self).setUp() + + +@ddt.ddt +class DevStackSystemdServiceTestCase(test.TestCase): + + def setUp(self): + super(DevStackSystemdServiceTestCase, self).setUp() + self.conf = {'address': '10.0.0.2', 'username': 'root'} + self.host = node_collection.Host('10.0.0.2') + self.discoverd_host = node_collection.Host(ip='10.0.0.2', + mac='09:7b:74:90:63:c1', + fqdn='') + + @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) + @ddt.data(*devstack_systemd.DevStackSystemdManagement.SERVICES.keys()) + def test_restart(self, service_name, 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'}, + host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')] + ] + + devstack_management = devstack_systemd.DevStackSystemdManagement( + self.conf) + + service = devstack_management.get_service(service_name) + service.restart() + + cmd = 'bash -c "ps ax | grep -v grep | grep \'{}\'"'.format( + service.grep) + ansible_runner_inst.execute.assert_has_calls([ + mock.call( + [self.host], {'command': 'cat /sys/class/net/eth0/address'}), + mock.call([self.discoverd_host], {'command': cmd}, []), + mock.call([self.discoverd_host], {'shell': service.restart_cmd}) + ]) + + @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) + @ddt.data(*devstack_systemd.DevStackSystemdManagement.SERVICES.keys()) + def test_terminate(self, service_name, 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'}, + host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')] + ] + + devstack_management = devstack_systemd.DevStackSystemdManagement( + self.conf) + + service = devstack_management.get_service(service_name) + service.terminate() + + cmd = 'bash -c "ps ax | grep -v grep | grep \'{}\'"'.format( + service.grep) + ansible_runner_inst.execute.assert_has_calls([ + mock.call( + [self.host], {'command': 'cat /sys/class/net/eth0/address'}), + mock.call([self.discoverd_host], {'command': cmd}, []), + mock.call([self.discoverd_host], {'shell': service.terminate_cmd}) + ]) + + @mock.patch('os_faults.ansible.executor.AnsibleRunner', autospec=True) + @ddt.data(*devstack_systemd.DevStackSystemdManagement.SERVICES.keys()) + def test_start(self, service_name, 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'}, + host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')], + [fakes.FakeAnsibleResult(payload={'stdout': ''}, host='10.0.0.2')] + ] + + devstack_management = devstack_systemd.DevStackSystemdManagement( + self.conf) + + service = devstack_management.get_service(service_name) + service.start() + + cmd = 'bash -c "ps ax | grep -v grep | grep \'{}\'"'.format( + service.grep) + ansible_runner_inst.execute.assert_has_calls([ + mock.call( + [self.host], {'command': 'cat /sys/class/net/eth0/address'}), + mock.call([self.discoverd_host], {'command': cmd}, []), + mock.call([self.discoverd_host], {'shell': service.start_cmd}) + ]) diff --git a/os_faults/tests/unit/test_os_faults.py b/os_faults/tests/unit/test_os_faults.py index 0adfd26..c2c6088 100644 --- a/os_faults/tests/unit/test_os_faults.py +++ b/os_faults/tests/unit/test_os_faults.py @@ -23,6 +23,7 @@ from os_faults.api import error from os_faults.api import node_collection from os_faults.api import service from os_faults.drivers import devstack +from os_faults.drivers import devstack_systemd from os_faults.drivers import fuel from os_faults.drivers import ipmi from os_faults.drivers import libvirt_driver @@ -65,6 +66,21 @@ class OSFaultsTestCase(test.TestCase): destructor = os_faults.connect(cloud_config) self.assertIsInstance(destructor, devstack.DevStackManagement) + def test_connect_devstack_systemd(self): + cloud_config = { + 'cloud_management': { + 'driver': 'devstack_systemd', + 'args': { + 'address': 'devstack.local', + 'username': 'developer', + 'private_key_file': '/my/path/pk.key', + } + } + } + destructor = os_faults.connect(cloud_config) + self.assertIsInstance(destructor, + devstack_systemd.DevStackSystemdManagement) + def test_config_with_services(self): self.cloud_config['services'] = { 'app': {