diff --git a/.zuul.yaml b/.zuul.yaml deleted file mode 100644 index 7051aeeb..00000000 --- a/.zuul.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- project: - templates: - - python35-charm-jobs diff --git a/src/lib/charm/openstack/octavia.py b/src/lib/charm/openstack/octavia.py new file mode 100644 index 00000000..c5d32d04 --- /dev/null +++ b/src/lib/charm/openstack/octavia.py @@ -0,0 +1,97 @@ +# Copyright 2018 Canonical Ltd +# +# 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 os +import subprocess + +import charms_openstack.charm +import charms_openstack.adapters +import charms_openstack.ip as os_ip + +import charmhelpers.core.host as ch_host + +OCTAVIA_DIR = '/etc/octavia' +OCTAVIA_CONF = os.path.join(OCTAVIA_DIR, 'octavia.conf') +OCTAVIA_WEBSERVER_SITE = 'octavia-api' +OCTAVIA_WSGI_CONF = '/etc/apache2/sites-available/octavia-api.conf' + +charms_openstack.charm.use_defaults('charm.default-select-release') + + +class OctaviaAdapters(charms_openstack.adapters.OpenStackAPIRelationAdapters): + """Adapters class for the Octavia charm.""" + def __init__(self, relations, charm_instance=None): + super(OctaviaAdapters, self).__init__( + relations, + options_instance=charms_openstack.adapters.APIConfigurationAdapter( + service_name='octavia', + port_map=OctaviaCharm.api_ports), + charm_intance=charm_instance) + + +class OctaviaCharm(charms_openstack.charm.HAOpenStackCharm): + """Charm class for the Octavia charm.""" + # layer-openstack-api uses service_type as service name in endpoint catalog + service_type = 'octavia' + release = 'rocky' + packages = ['octavia-api', 'octavia-health-manager', + 'octavia-housekeeping', 'octavia-worker', + 'apache2', 'libapache2-mod-wsgi-py3'] + python_version = 3 + api_ports = { + 'octavia-api': { + os_ip.PUBLIC: 9876, + os_ip.ADMIN: 9876, + os_ip.INTERNAL: 9876, + }, + } + default_service = 'octavia-api' + services = ['apache2', 'octavia-health-manager', 'octavia-housekeeping', + 'octavia-worker'] + required_relations = ['shared-db', 'amqp', 'identity-service'] + restart_map = { + OCTAVIA_CONF: services, + OCTAVIA_WSGI_CONF: ['apache2'], + } + sync_cmd = ['sudo', 'octavia-db-manage', 'upgrade', 'head'] + ha_resources = ['vips', 'haproxy', 'dnsha'] + release_pkg = 'octavia-common' + package_codenames = { + 'octavia-common': collections.OrderedDict([ + ('1', 'rocky'), + ]), + } + group = 'octavia' + + def get_amqp_credentials(self): + """Configure the AMQP credentials for Octavia.""" + return ('octavia', 'openstack') + + def get_database_setup(self): + """Configure the database credentials for Octavia.""" + return [{'database': 'octavia', + 'username': 'octavia'}] + + def enable_webserver_site(self): + """Enable Octavia API apache2 site if rendered or installed""" + if os.path.exists(OCTAVIA_WSGI_CONF): + check_enabled = subprocess.call( + ['a2query', '-s', OCTAVIA_WEBSERVER_SITE] + ) + if check_enabled != 0: + subprocess.check_call(['a2ensite', + OCTAVIA_WEBSERVER_SITE]) + ch_host.service_reload('apache2', + restart_on_failure=True) diff --git a/src/reactive/octavia_handlers.py b/src/reactive/octavia_handlers.py new file mode 100644 index 00000000..9e95a912 --- /dev/null +++ b/src/reactive/octavia_handlers.py @@ -0,0 +1,53 @@ +# Copyright 2018 Canonical Ltd +# +# 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 charms.reactive as reactive + +import charms_openstack.charm as charm + +import charm.openstack.octavia as octavia # noqa + +charm.use_defaults( + 'charm.installed', + 'amqp.connected', + 'shared-db.connected', + 'identity-service.connected', + 'identity-service.available', + 'config.changed', + 'update-status') + + +@reactive.when('shared-db.available') +@reactive.when('identity-service.available') +@reactive.when('amqp.available') +def render(*args): + """ + Render the configuration for Octavia when all interfaces are available. + """ + with charm.provide_charm_instance() as octavia_charm: + octavia_charm.render_with_interfaces(args) + octavia_charm.enable_webserver_site() + octavia_charm.assess_status() + reactive.set_state('config.rendered') + + +@reactive.when_not('db.synced') +@reactive.when('config.rendered') +def init_db(): + """Run initial DB migrations when config is rendered.""" + with charm.provide_charm_instance() as octavia_charm: + octavia_charm.db_sync() + octavia_charm.restart_all() + reactive.set_state('db.synced') + octavia_charm.assess_status() diff --git a/src/templates/rocky/octavia-api.conf b/src/templates/rocky/octavia-api.conf new file mode 100644 index 00000000..c8fcddad --- /dev/null +++ b/src/templates/rocky/octavia-api.conf @@ -0,0 +1,19 @@ +Listen {{ options.service_listen_info.octavia_api.public_port }} + +# workaround problem with Python Cryptography and libssl1.0.0 by adding +# WSGIApplicationGroup %{GLOBAL} +# See https://cryptography.io/en/latest/faq/#starting-cryptography-using-mod-wsgi-produces-an-internalerror-during-a-call-in-register-osrandom-engine + + + WSGIDaemonProcess octavia-api user=octavia group=octavia processes=3 threads=10 + WSGIProcessGroup octavia-api + WSGIApplicationGroup %{GLOBAL} + WSGIScriptAlias / /usr/bin/octavia-wsgi + + + Require all granted + + + ErrorLog /var/log/apache2/octavia_error.log + CustomLog /var/log/apache2/octavia_access.log combined + diff --git a/src/templates/rocky/octavia.conf b/src/templates/rocky/octavia.conf new file mode 100644 index 00000000..60d74f6d --- /dev/null +++ b/src/templates/rocky/octavia.conf @@ -0,0 +1,14 @@ +[DEFAULT] +debug = {{ options.debug }} + +[database] +{% include "parts/database" %} + +{% include "parts/section-keystone-authtoken" %} + +[oslo_messaging] +topic = octavia + +{% include "parts/section-rabbitmq-oslo" %} + +{% include "parts/section-oslo-middleware" %} diff --git a/src/tests/bundles/smoke-bionic-rocky.yaml b/src/tests/bundles/smoke-bionic-rocky.yaml new file mode 100644 index 00000000..4a36a4e3 --- /dev/null +++ b/src/tests/bundles/smoke-bionic-rocky.yaml @@ -0,0 +1,27 @@ +series: bionic +relations: +- - keystone + - mysql +- - octavia + - mysql +- - octavia + - keystone +- - octavia + - rabbitmq-server +applications: + keystone: + charm: cs:~openstack-charmers-next/keystone + num_units: 1 + options: + openstack-origin: cloud:bionic-rocky + mysql: + charm: cs:~openstack-charmers-next/percona-cluster + num_units: 1 + octavia: + charm: octavia + num_units: 1 + options: + openstack-origin: cloud:bionic-rocky + rabbitmq-server: + charm: cs:~openstack-charmers-next/rabbitmq-server + num_units: 1 diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml new file mode 100644 index 00000000..dbf59d7b --- /dev/null +++ b/src/tests/tests.yaml @@ -0,0 +1,5 @@ +charm_name: octavia +smoke_bundles: +- smoke-bionic-rocky +configure: +tests: diff --git a/unit_tests/test_lib_charm_openstack_octavia.py b/unit_tests/test_lib_charm_openstack_octavia.py new file mode 100644 index 00000000..b70a6b88 --- /dev/null +++ b/unit_tests/test_lib_charm_openstack_octavia.py @@ -0,0 +1,41 @@ +# Copyright 2018 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import charms_openstack.test_utils as test_utils + +import charm.openstack.octavia as octavia + + +class Helper(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(octavia.OctaviaCharm.release) + + +class TestOctaviaCharm(Helper): + + def test_get_amqp_credentials(self): + c = octavia.OctaviaCharm() + result = c.get_amqp_credentials() + self.assertEqual(result, ('octavia', 'openstack')) + + def test_get_database_setup(self): + c = octavia.OctaviaCharm() + result = c.get_database_setup() + self.assertEqual(result, [{'database': 'octavia', + 'username': 'octavia'}]) diff --git a/unit_tests/test_octavia_handlers.py b/unit_tests/test_octavia_handlers.py new file mode 100644 index 00000000..8c9d9fe0 --- /dev/null +++ b/unit_tests/test_octavia_handlers.py @@ -0,0 +1,71 @@ +# Copyright 2018 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import mock + +import reactive.octavia_handlers as handlers + +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [ + 'charm.installed', + 'amqp.connected', + 'shared-db.connected', + 'identity-service.connected', + 'identity-service.available', + 'config.changed', + 'update-status'] + hook_set = { + 'when': { + 'render': ('shared-db.available', + 'identity-service.available', + 'amqp.available'), + 'init_db': ('config.rendered'), + }, + 'when_not': { + 'init_db': ('db.synced'), + }, + } + # test that the hooks were registered via the + # reactive.octavia_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults) + + +class TestRender(test_utils.PatchHelper): + + def test_render(self): + octavia_charm = mock.MagicMock() + self.patch_object(handlers.charm, 'provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = octavia_charm + self.provide_charm_instance().__exit__.return_value = None + self.patch_object(handlers.charm, 'optional_interfaces') + + def _optional_interfaces(args, *interfaces): + self.assertEqual(interfaces, ('tls-certificates.available', )) + return args + ('tls-certificates', ) + + self.optional_interfaces.side_effect = _optional_interfaces + + handlers.render('arg1', 'arg2') + octavia_charm.render_with_interfaces.assert_called_once_with( + ('arg1', 'arg2', 'tls-certificates')) + octavia_charm.assess_status.assert_called_once_with()