Add OPS framework interface
Change-Id: If5efdd7b91661ae6b5ac9aa6c43151ce9f4577ec
This commit is contained in:
parent
679a76dfe5
commit
1730dfe3fd
|
@ -11,3 +11,5 @@ ignore:
|
||||||
- 'tox.ini'
|
- 'tox.ini'
|
||||||
- 'unit_tests'
|
- 'unit_tests'
|
||||||
- '.zuul.yaml'
|
- '.zuul.yaml'
|
||||||
|
- 'setup.cfg'
|
||||||
|
- 'setup.py'
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
# 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.
|
||||||
|
# Copyright 2021 Ubuntu
|
||||||
|
# See LICENSE file for licensing details.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import interface_hacluster.common as common
|
||||||
|
|
||||||
|
from ops.framework import (
|
||||||
|
StoredState,
|
||||||
|
EventBase,
|
||||||
|
ObjectEvents,
|
||||||
|
EventSource,
|
||||||
|
Object)
|
||||||
|
|
||||||
|
|
||||||
|
class HAServiceReadyEvent(EventBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HAServiceEvents(ObjectEvents):
|
||||||
|
ha_ready = EventSource(HAServiceReadyEvent)
|
||||||
|
|
||||||
|
|
||||||
|
class HAServiceRequires(Object, common.ResourceManagement):
|
||||||
|
|
||||||
|
on = HAServiceEvents()
|
||||||
|
_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)
|
||||||
|
self._stored.set_default(
|
||||||
|
resources={})
|
||||||
|
|
||||||
|
def get_local(self, key, default=None):
|
||||||
|
key = '%s.%s' % ('local-data', key)
|
||||||
|
json_value = getattr(self._stored, key, None)
|
||||||
|
if json_value:
|
||||||
|
return json.loads(json_value)
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_local(self, key=None, value=None, data=None, **kwdata):
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
if key is not None:
|
||||||
|
data[key] = value
|
||||||
|
data.update(kwdata)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
for k, v in data.items():
|
||||||
|
setattr(
|
||||||
|
self._stored,
|
||||||
|
'local-data.{}'.format(k),
|
||||||
|
json.dumps(v))
|
||||||
|
|
||||||
|
def _on_relation_changed(self, event):
|
||||||
|
if self.is_clustered():
|
||||||
|
self.on.ha_ready.emit()
|
||||||
|
|
||||||
|
def data_changed(self, data_id, data, hash_type='md5'):
|
||||||
|
key = 'data_changed.%s' % data_id
|
||||||
|
alg = getattr(hashlib, hash_type)
|
||||||
|
serialized = json.dumps(data, sort_keys=True).encode('utf8')
|
||||||
|
old_hash = self.get_local(key)
|
||||||
|
new_hash = alg(serialized).hexdigest()
|
||||||
|
self.set_local(key, new_hash)
|
||||||
|
return old_hash != new_hash
|
||||||
|
|
||||||
|
def set_remote(self, key=None, value=None, data=None, **kwdata):
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
if key is not None:
|
||||||
|
data[key] = value
|
||||||
|
data.update(kwdata)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
for relation in self.framework.model.relations[self.relation_name]:
|
||||||
|
for k, v in data.items():
|
||||||
|
# The reactive framework copes with integer values but the ops
|
||||||
|
# framework insists on strings so convert them.
|
||||||
|
if isinstance(v, int):
|
||||||
|
v = str(v)
|
||||||
|
relation.data[self.model.unit][k] = v
|
||||||
|
|
||||||
|
def get_remote_all(self, key, default=None):
|
||||||
|
"""Return a list of all values presented by remote units for key"""
|
||||||
|
values = []
|
||||||
|
for relation in self.framework.model.relations[self.relation_name]:
|
||||||
|
for unit in relation.units:
|
||||||
|
value = relation.data[unit].get(key)
|
||||||
|
if value:
|
||||||
|
values.append(value)
|
||||||
|
return list(set(values))
|
|
@ -0,0 +1,18 @@
|
||||||
|
[metadata]
|
||||||
|
name = interface_hacluster
|
||||||
|
summary = Charm interface for Hacluster 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/charm-interface-hacluster.git
|
||||||
|
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
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Module used to setup the interface_hacluster 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,
|
||||||
|
)
|
|
@ -4,3 +4,4 @@ stestr>=2.2.0
|
||||||
charms.reactive
|
charms.reactive
|
||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
netifaces
|
netifaces
|
||||||
|
git+https://github.com/canonical/operator.git#egg=ops
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# Copyright 2021 Ubuntu
|
||||||
|
# See LICENSE file for licensing details.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
sys.path.append('.') # noqa
|
||||||
|
from ops.testing import Harness
|
||||||
|
from ops.charm import CharmBase
|
||||||
|
import interface_hacluster.ops_ha_interface as ops_ha_interface
|
||||||
|
|
||||||
|
|
||||||
|
class HAServiceRequires(unittest.TestCase):
|
||||||
|
|
||||||
|
class MyCharm(CharmBase):
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self.seen_events = []
|
||||||
|
self.ha = ops_ha_interface.HAServiceRequires(self, 'ha')
|
||||||
|
|
||||||
|
self.framework.observe(
|
||||||
|
self.ha.on.ha_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:
|
||||||
|
ha:
|
||||||
|
interface: hacluster
|
||||||
|
scope: container
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_local_vars(self):
|
||||||
|
self.harness.begin()
|
||||||
|
self.harness.charm.ha.set_local('a', 'b')
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_local('a'),
|
||||||
|
'b')
|
||||||
|
self.harness.charm.ha.set_local(**{'c': 'd', 'e': 'f'})
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_local('c'),
|
||||||
|
'd')
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_local('e'),
|
||||||
|
'f')
|
||||||
|
self.harness.charm.ha.set_local(data={'g': 'h', 'i': 'j'})
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_local('g'),
|
||||||
|
'h')
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_local('i'),
|
||||||
|
'j')
|
||||||
|
|
||||||
|
def test_remote_vars(self):
|
||||||
|
self.harness.begin()
|
||||||
|
rel_id = self.harness.add_relation(
|
||||||
|
'ha',
|
||||||
|
'hacluster')
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id,
|
||||||
|
'hacluster/0')
|
||||||
|
self.harness.charm.ha.set_remote('a', 'b')
|
||||||
|
rel_data = self.harness.get_relation_data(
|
||||||
|
rel_id,
|
||||||
|
'my-charm/0')
|
||||||
|
self.assertEqual(rel_data, {'a': 'b'})
|
||||||
|
|
||||||
|
def test_get_remote_all(self):
|
||||||
|
self.harness.begin()
|
||||||
|
rel_id1 = self.harness.add_relation(
|
||||||
|
'ha',
|
||||||
|
'hacluster-a')
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id1,
|
||||||
|
'hacluster-a/0')
|
||||||
|
self.harness.update_relation_data(
|
||||||
|
rel_id1,
|
||||||
|
'hacluster-a/0',
|
||||||
|
{'fruit': 'banana'})
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id1,
|
||||||
|
'hacluster-a/1')
|
||||||
|
self.harness.update_relation_data(
|
||||||
|
rel_id1,
|
||||||
|
'hacluster-a/1',
|
||||||
|
{'fruit': 'orange'})
|
||||||
|
rel_id2 = self.harness.add_relation(
|
||||||
|
'ha',
|
||||||
|
'hacluster-b')
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id2,
|
||||||
|
'hacluster-b/0')
|
||||||
|
self.harness.update_relation_data(
|
||||||
|
rel_id2,
|
||||||
|
'hacluster-b/0',
|
||||||
|
{'fruit': 'grape'})
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id2,
|
||||||
|
'hacluster-b/1')
|
||||||
|
self.harness.update_relation_data(
|
||||||
|
rel_id2,
|
||||||
|
'hacluster-b/1',
|
||||||
|
{'veg': 'carrot'})
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.ha.get_remote_all('fruit'),
|
||||||
|
['orange', 'grape', 'banana'])
|
||||||
|
|
||||||
|
def test_ha_ready(self):
|
||||||
|
self.harness.begin()
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.seen_events,
|
||||||
|
[])
|
||||||
|
rel_id = self.harness.add_relation(
|
||||||
|
'ha',
|
||||||
|
'hacluster')
|
||||||
|
self.harness.add_relation_unit(
|
||||||
|
rel_id,
|
||||||
|
'hacluster/0')
|
||||||
|
self.harness.update_relation_data(
|
||||||
|
rel_id,
|
||||||
|
'hacluster/0',
|
||||||
|
{'clustered': 'yes'})
|
||||||
|
self.assertEqual(
|
||||||
|
self.harness.charm.seen_events,
|
||||||
|
['HAServiceReadyEvent'])
|
||||||
|
|
||||||
|
def test_data_changed(self):
|
||||||
|
self.harness.begin()
|
||||||
|
self.assertTrue(
|
||||||
|
self.harness.charm.ha.data_changed(
|
||||||
|
'relation-data', {'a': 'b'}))
|
||||||
|
self.assertFalse(
|
||||||
|
self.harness.charm.ha.data_changed(
|
||||||
|
'relation-data', {'a': 'b'}))
|
||||||
|
self.assertTrue(
|
||||||
|
self.harness.charm.ha.data_changed(
|
||||||
|
'relation-data', {'a': 'c'}))
|
Loading…
Reference in New Issue