Add OPS framework interface
Add OPS framework support and exclude ops framework file from reactive builds. Change-Id: I79ae194b97483072501e07e4f8c02bdad03bc19f
This commit is contained in:
parent
679a76dfe5
commit
34b12aa480
@ -11,3 +11,6 @@ ignore:
|
||||
- 'tox.ini'
|
||||
- 'unit_tests'
|
||||
- '.zuul.yaml'
|
||||
- 'setup.cfg'
|
||||
- 'setup.py'
|
||||
- '**/ops_ha_interface.py'
|
||||
|
0
interface_hacluster/__init__.py
Normal file
0
interface_hacluster/__init__.py
Normal file
123
interface_hacluster/ops_ha_interface.py
Normal file
123
interface_hacluster/ops_ha_interface.py
Normal file
@ -0,0 +1,123 @@
|
||||
# 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'):
|
||||
"""Check if the given set of data has changed since the previous call.
|
||||
|
||||
This works by hashing the JSON-serialization of the data. Note that,
|
||||
while the data will be serialized using ``sort_keys=True``, some types
|
||||
of data structures, such as sets, may lead to false positivies.
|
||||
|
||||
NOTE: This method is adapted from the charms.reactive framework and
|
||||
is used by the parent class `common.ResourceManagement`. It is unlikely
|
||||
that a charm utilising this interface would call this method directly.
|
||||
|
||||
:param str data_id: Unique identifier for this set of data.
|
||||
:param data: JSON-serializable data.
|
||||
:param str hash_type: Any hash algorithm supported by :mod:`hashlib`.
|
||||
"""
|
||||
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))
|
18
setup.cfg
Normal file
18
setup.cfg
Normal file
@ -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
|
38
setup.py
Normal file
38
setup.py
Normal file
@ -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
|
||||
coverage>=3.6
|
||||
netifaces
|
||||
git+https://github.com/canonical/operator.git#egg=ops
|
||||
|
158
unit_tests/test_ops.py
Normal file
158
unit_tests/test_ops.py
Normal file
@ -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…
x
Reference in New Issue
Block a user