commit e894183770c3d3f42721a2717a632c3af270d7ae Author: Liam Young Date: Tue Sep 7 14:37:29 2021 +0000 First cut diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01ade97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.tox +.stestr/ +__pycache__ diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/interface_ceph_iscsi_admin_access/__init__.py b/interface_ceph_iscsi_admin_access/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/interface_ceph_iscsi_admin_access/admin_access.py b/interface_ceph_iscsi_admin_access/admin_access.py new file mode 100644 index 0000000..ada4dd4 --- /dev/null +++ b/interface_ceph_iscsi_admin_access/admin_access.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +# Copyright 2021 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 ops.framework import ( + StoredState, + EventBase, + ObjectEvents, + EventSource, + Object) + + +class CephISCSIAdminAccessEvent(EventBase): + pass + + +class CephISCSIAdminAccessEvents(ObjectEvents): + admin_access_ready = EventSource(CephISCSIAdminAccessEvent) + admin_access_request = EventSource(CephISCSIAdminAccessEvent) + + +class CephISCSIAdminAccessRequires(Object): + + on = CephISCSIAdminAccessEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name): + super().__init__(charm, relation_name) + self.relation_name = relation_name + self.framework.observe( + charm.on[self.relation_name].relation_changed, + self._on_relation_changed) + + def get_user_creds(self): + creds = [] + for relation in self.framework.model.relations[self.relation_name]: + app_data = relation.data[relation.app] + for unit in relation.units: + unit_data = relation.data[unit] + cred_data = { + 'name': unit_data.get('name'), + 'host': unit_data.get('host'), + 'username': app_data.get('username'), + 'password': app_data.get('password'), + 'scheme': unit_data.get('scheme'), + 'port': unit_data.get('port')} + if all(cred_data.values()): + creds.append(cred_data) + creds = sorted(creds, key=lambda k: k['host']) + return creds + + def _on_relation_changed(self, event): + """Handle the relation-changed event.""" + if self.get_user_creds(): + self.on.admin_access_ready.emit() + + +class CephISCSIAdminAccessProvides(Object): + + on = CephISCSIAdminAccessEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name): + super().__init__(charm, relation_name) + self.relation_name = relation_name + self.framework.observe( + charm.on[self.relation_name].relation_joined, + self._on_relation_joined) + + def get_admin_access_requests(self): + usernames = [ + f"{r.name}-{r.id}" + for r in self.framework.model.relations[self.relation_name]] + return usernames + + def _on_relation_joined(self, event): + """Handle the relation-changed event.""" + if self.get_admin_access_requests(): + self.on.admin_access_request.emit() + + def publish_gateway(self, name, username, password, scheme, port=5000): + for relation in self.framework.model.relations[self.relation_name]: + if self.model.unit.is_leader(): + relation.data[self.model.app]['username'] = username + relation.data[self.model.app]['password'] = password + binding = self.framework.model.get_binding(relation) + relation.data[self.model.unit]['name'] = name + relation.data[self.model.unit]['scheme'] = scheme + relation.data[self.model.unit]['port'] = str(port) + relation.data[self.model.unit]['host'] = str( + binding.network.bind_address) + + @property + def client_addresses(self): + addressees = [] + for relation in self.framework.model.relations[self.relation_name]: + for unit in relation.units: + addressees.append(relation.data[unit]['ingress-address']) + return sorted(addressees) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cea68bd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +name = interface_ceph_iscsi_admin_access +summary = Charm interface for Ceph iSCSI admin access using Operator Framework +version = 0.0.1.dev1 +description-file = + README.rst +author = OpenStack Charmers +author-email = openstack-charmers@lists.ubuntu.com +url = https://github.com/openstack-charmers/ops-interface-ceph-iscsi-admin-access +classifier = + Development Status :: 2 - Pre-Alpha + Intended Audience :: Developers + Topic :: System + Topic :: System :: Installation/Setup + opic :: System :: Software Distribution + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + License :: OSI Approved :: Apache Software License diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..876a7aa --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# Copyright 2020 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. + +"""Module used to setup the interface_ceph_client framework.""" + +from __future__ import print_function + +from setuptools import setup, find_packages + +version = "0.0.1.dev1" +install_require = [ + 'charmhelpers', + 'ops', +] + +tests_require = [ + 'tox >= 2.3.1', +] + +setup( + license='Apache-2.0: http://www.apache.org/licenses/LICENSE-2.0', + packages=find_packages(exclude=["unit_tests"]), + zip_safe=False, + install_requires=install_require, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..f66177f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,9 @@ +# Lint and unit test requirements +flake8 +stestr>=2.2.0 +mock>=1.2 +coverage>=3.6 +# Install netifaces as its a horrible charmhelpers lazy import +netifaces +charmhelpers +git+https://github.com/canonical/operator.git#egg=ops diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d5f0b2c --- /dev/null +++ b/tox.ini @@ -0,0 +1,59 @@ +[tox] +skipsdist = True +envlist = pep8,py3 +# NOTE(beisner): Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE(beisner): Avoid false positives by not skipping missing interpreters. +skip_missing_interpreters = False + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 + TERM=linux +install_command = + pip install {opts} {packages} + +[testenv:py3] +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run {posargs} + +[testenv:pep8] +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} . unit_tests + +[testenv:cover] +# Technique based heavily upon +# https://github.com/openstack/nova/blob/master/tox.ini +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +setenv = + {[testenv]setenv} + PYTHON=coverage run +commands = + coverage erase + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[coverage:run] +branch = True +concurrency = multiprocessing +parallel = True +source = + . +omit = + .tox/* + */charmhelpers/* + unit_tests/* + +[testenv:venv] +basepython = python3 +commands = {posargs} + +[flake8] +# E402 ignore necessary for path append before sys module import in actions +ignore = E402 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unit_tests/test_interface_iscsi_admin_access.py b/unit_tests/test_interface_iscsi_admin_access.py new file mode 100644 index 0000000..6c5f2f9 --- /dev/null +++ b/unit_tests/test_interface_iscsi_admin_access.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 + +# Copyright 2021 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 unittest +import sys +sys.path.append('lib') # noqa +sys.path.append('src') # noqa + +from ops.testing import Harness, _TestingModelBackend +from ops.charm import CharmBase +from ops import framework, model + +from interface_ceph_iscsi_admin_access.admin_access import ( + CephISCSIAdminAccessRequires, + CephISCSIAdminAccessProvides) + + +class TestCephISCSIAdminAccessRequires(unittest.TestCase): + + class MyCharm(CharmBase): + + def __init__(self, *args): + super().__init__(*args) + self.seen_events = [] + self.iscsi_user = CephISCSIAdminAccessRequires( + self, + 'iscsi-dashboard') + self.framework.observe( + self.iscsi_user.on.admin_access_ready, + self._log_event) + + def _log_event(self, event): + self.seen_events.append(type(event).__name__) + + def setUp(self): + super().setUp() + self.harness = Harness( + self.MyCharm, + meta=''' +name: my-charm +requires: + iscsi-dashboard: + interface: admin-access +''' + ) + + def test_init(self): + self.harness.begin() + self.assertEqual( + self.harness.charm.iscsi_user.relation_name, + 'iscsi-dashboard') + + def add_iscsi_relation(self, iscsi_app_name='ceph-iscsi', + complete=True): + rel_id = self.harness.add_relation( + 'iscsi-dashboard', + iscsi_app_name) + self.harness.add_relation_unit( + rel_id, + '{}/0'.format(iscsi_app_name)) + if complete: + self.complete_relation(rel_id, iscsi_app_name) + return rel_id + + def complete_relation(self, rel_id, iscsi_app_name='ceph-iscsi'): + unit_name = '{}/0'.format(iscsi_app_name) + self.harness.update_relation_data( + rel_id, + unit_name, + { + 'name': unit_name.replace('/', '-'), + 'host': '{}1.foo'.format(iscsi_app_name), + 'scheme': 'http', + 'port': '23'}) + self.harness.update_relation_data( + rel_id, + iscsi_app_name, + { + 'username': 'admin', + 'password': 'password'}) + + def test_add_iscsi_dashboard_relation(self): + self.harness.begin() + self.harness.set_leader() + rel_id = self.add_iscsi_relation(complete=False) + self.assertEqual( + self.harness.charm.seen_events, + []) + self.complete_relation(rel_id) + self.assertEqual( + self.harness.charm.seen_events, + ['CephISCSIAdminAccessEvent']) + + def test_get_user_creds(self): + self.harness.begin() + self.harness.set_leader() + expect_east = { + 'host': 'ceph-iscsi-east1.foo', + 'name': 'ceph-iscsi-east-0', + 'password': 'password', + 'port': '23', + 'scheme': 'http', + 'username': 'admin'} + expect_west = { + 'host': 'ceph-iscsi-west1.foo', + 'name': 'ceph-iscsi-west-0', + 'password': 'password', + 'port': '23', + 'scheme': 'http', + 'username': 'admin'} + self.add_iscsi_relation('ceph-iscsi-east') + self.assertEqual( + self.harness.charm.iscsi_user.get_user_creds(), + [expect_east]) + self.add_iscsi_relation('ceph-iscsi-west') + self.assertEqual( + self.harness.charm.iscsi_user.get_user_creds(), + [expect_east, expect_west]) + + +class TestCephISCSIAdminAccessProvides(unittest.TestCase): + + class MyCharm(CharmBase): + + def __init__(self, *args): + super().__init__(*args) + self.seen_events = [] + + self.admin_access = CephISCSIAdminAccessProvides( + self, + 'admin-access') + self.framework.observe( + self.admin_access.on.admin_access_request, + self._log_event) + + def _log_event(self, event): + self.seen_events.append(type(event).__name__) + + def setUp(self): + super().setUp() + self.harness = Harness( + self.MyCharm, + meta=''' +name: ceph-iscsi +provides: + admin-access: + interface: admin-access +''' + ) + + # BEGIN: Workaround until network_get is implemented + class _TestingOPSModelBackend(_TestingModelBackend): + + def network_get(self, endpoint_name, relation_id=None): + network_data = { + 'bind-addresses': [{ + 'interface-name': 'eth0', + 'addresses': [{ + 'cidr': '10.0.0.0/24', + 'value': '10.0.0.10'}]}], + 'ingress-addresses': ['10.0.0.10'], + 'egress-subnets': ['10.0.0.0/24']} + return network_data + + self.harness._backend = _TestingOPSModelBackend( + self.harness._unit_name, self.harness._meta) + self.harness._model = model.Model( + self.harness._meta, + self.harness._backend) + self.harness._framework = framework.Framework( + ":memory:", + self.harness._charm_dir, + self.harness._meta, + self.harness._model) + # END Workaround + + def test_init(self): + self.harness.begin() + self.assertEqual( + self.harness.charm.admin_access.relation_name, + 'admin-access') + + def add_admin_access_relation(self, ingress_address, + app_name='ceph-dashboard'): + unit_name = '{}/0'.format(app_name) + rel_id = self.harness.add_relation( + 'admin-access', + app_name) + self.harness.add_relation_unit( + rel_id, + unit_name) + self.harness.update_relation_data( + rel_id, + unit_name, + {'ingress-address': ingress_address}) + return rel_id + + def test_get_admin_access_requests(self): + self.harness.begin() + self.add_admin_access_relation('10.0.0.12') + self.add_admin_access_relation('10.0.0.12', 'ceph-client') + self.assertEqual( + self.harness.charm.admin_access.get_admin_access_requests(), + ['admin-access-0', 'admin-access-1']) + + def test_client_addresses(self): + self.harness.begin() + self.add_admin_access_relation('10.0.0.12') + self.add_admin_access_relation('192.168.9.34', 'ceph-client') + self.assertEqual( + self.harness.charm.admin_access.client_addresses, + ['10.0.0.12', '192.168.9.34']) + + def test_publish_gateway(self): + self.harness.begin() + self.harness.set_leader() + rel_id1 = self.add_admin_access_relation('10.0.0.12') + rel_id2 = self.add_admin_access_relation('192.168.9.34', 'ceph-client') + self.harness.charm.admin_access.publish_gateway( + 'foo', + 'admin', + 'password', + 'http', + '5001') + unit_data_expect = { + 'host': '10.0.0.10', + 'name': 'foo', + 'port': '5001', + 'scheme': 'http'} + app_data_expect = { + 'password': 'password', + 'username': 'admin'} + self.assertEqual( + self.harness.get_relation_data(rel_id1, 'ceph-iscsi/0'), + unit_data_expect) + self.assertEqual( + self.harness.get_relation_data(rel_id1, 'ceph-iscsi'), + app_data_expect) + self.assertEqual( + self.harness.get_relation_data(rel_id2, 'ceph-iscsi/0'), + unit_data_expect) + self.assertEqual( + self.harness.get_relation_data(rel_id2, 'ceph-iscsi'), + app_data_expect)