First cut

This commit is contained in:
Liam Young
2021-09-07 14:37:29 +00:00
commit e894183770
10 changed files with 498 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.tox
.stestr/
__pycache__

3
.stestr.conf Normal file
View File

@@ -0,0 +1,3 @@
[DEFAULT]
test_path=./unit_tests
top_dir=./

View File

@@ -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)

18
setup.cfg Normal file
View File

@@ -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

38
setup.py Normal file
View File

@@ -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,
)

9
test-requirements.txt Normal file
View File

@@ -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

59
tox.ini Normal file
View File

@@ -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

0
unit_tests/__init__.py Normal file
View File

View File

@@ -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)