Move common requires code

Move common requires code in to the common module so that
requires.py only contains the code which is specific to
reactive charms. This will allow for a subsequent patch which
creates a requires interface consumable by operator framework
charms.

Change-Id: I70037252cc7a677a9394929cb0cd17e9506ab624
This commit is contained in:
Liam Young 2021-08-23 07:50:43 +00:00
parent 6c611a3c61
commit 679a76dfe5
5 changed files with 263 additions and 240 deletions

View File

@ -13,9 +13,258 @@
import hashlib
import ipaddress
import json
from six import string_types
class ResourceManagement():
def data_changed(self, data_id, data, hash_type='md5'):
raise NotImplementedError
def get_local(self, key, default=None, scope=None):
raise NotImplementedError
def set_local(self, key=None, value=None, data=None, scope=None, **kwdata):
raise NotImplementedError
def set_remote(self, key=None, value=None, data=None, scope=None,
**kwdata):
raise NotImplementedError
def is_clustered(self):
"""Has the hacluster charm set clustered?
The hacluster charm sets cluster=True when it determines it is ready.
Check the relation data for clustered and force a boolean return.
:returns: boolean
"""
clustered_values = self.get_remote_all('clustered')
if clustered_values:
# There is only ever one subordinate hacluster unit
clustered = clustered_values[0]
# Future versions of hacluster will return a bool
# Current versions return a string
if type(clustered) is bool:
return clustered
elif (clustered is not None and
(clustered.lower() == 'true' or
clustered.lower() == 'yes')):
return True
return False
def bind_on(self, iface=None, mcastport=None):
relation_data = {}
if iface:
relation_data['corosync_bindiface'] = iface
if mcastport:
relation_data['corosync_mcastport'] = mcastport
if relation_data and self.data_changed('hacluster-bind_on',
relation_data):
self.set_local(**relation_data)
self.set_remote(**relation_data)
def manage_resources(self, crm):
"""
Request for the hacluster to manage the resources defined in the
crm object.
res = CRM()
res.primitive('res_neutron_haproxy', 'lsb:haproxy',
op='monitor interval="5s"')
res.init_services('haproxy')
res.clone('cl_nova_haproxy', 'res_neutron_haproxy')
hacluster.manage_resources(crm)
:param crm: CRM() instance - Config object for Pacemaker resources
:returns: None
"""
relation_data = {
'json_{}'.format(k): json.dumps(v, sort_keys=True)
for k, v in crm.items() if v
}
if self.data_changed('hacluster-manage_resources', relation_data):
self.set_local(**relation_data)
self.set_remote(**relation_data)
def bind_resources(self, iface=None, mcastport=None):
"""Inform the ha subordinate about each service it should manage. The
child class specifies the services via self.ha_resources
:param iface: string - Network interface to bind to
:param mcastport: int - Multicast port corosync should use for cluster
management traffic
"""
if mcastport is None:
mcastport = 4440
resources_dict = self.get_local('resources')
self.bind_on(iface=iface, mcastport=mcastport)
if resources_dict:
resources = CRM(**resources_dict)
self.manage_resources(resources)
def delete_resource(self, resource_name):
resource_dict = self.get_local('resources')
if resource_dict:
resources = CRM(**resource_dict)
else:
resources = CRM()
resources.add_delete_resource(resource_name)
self.set_local(resources=resources)
def add_vip(self, name, vip, iface=None, netmask=None):
"""Add a VirtualIP object for each user specified vip to self.resources
:param name: string - Name of service
:param vip: string - Virtual IP to be managed
:param iface: string - Network interface to bind vip to
:param netmask: string - Netmask for vip
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = CRM(**resource_dict)
else:
resources = CRM()
resources.add(
VirtualIP(
name,
vip,
nic=iface,
cidr=netmask,))
# Vip Group
group = 'grp_{}_vips'.format(name)
vip_res_group_members = []
if resource_dict:
vip_resources = resource_dict.get('resources')
if vip_resources:
for vip_res in vip_resources:
if 'vip' in vip_res:
vip_res_group_members.append(vip_res)
resources.group(group,
*sorted(vip_res_group_members))
self.set_local(resources=resources)
def remove_vip(self, name, vip, iface=None):
"""Remove a virtual IP
:param name: string - Name of service
:param vip: string - Virtual IP
:param iface: string - Network interface vip bound to
"""
if iface:
nic_name = iface
else:
nic_name = hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7]
self.delete_resource('res_{}_{}_vip'.format(name, nic_name))
def add_init_service(self, name, service, clone=True):
"""Add a InitService object for haproxy to self.resources
:param name: string - Name of service
:param service: string - Name service uses in init system
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = CRM(**resource_dict)
else:
resources = CRM()
resources.add(
InitService(name, service, clone))
self.set_local(resources=resources)
def remove_init_service(self, name, service):
"""Remove an init service
:param name: string - Name of service
:param service: string - Name of service used in init system
"""
res_key = 'res_{}_{}'.format(
name.replace('-', '_'),
service.replace('-', '_'))
self.delete_resource(res_key)
def add_systemd_service(self, name, service, clone=True):
"""Add a SystemdService object to self.resources
:param name: string - Name of service
:param service: string - Name service uses in systemd
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = CRM(**resource_dict)
else:
resources = CRM()
resources.add(
SystemdService(name, service, clone))
self.set_local(resources=resources)
def remove_systemd_service(self, name, service):
"""Remove a systemd service
:param name: string - Name of service
:param service: string - Name of service used in systemd
"""
res_key = 'res_{}_{}'.format(
name.replace('-', '_'),
service.replace('-', '_'))
self.delete_resource(res_key)
def add_dnsha(self, name, ip, fqdn, endpoint_type):
"""Add a DNS entry to self.resources
:param name: string - Name of service
:param ip: string - IP address dns entry should resolve to
:param fqdn: string - The DNS entry name
:param endpoint_type: string - Public, private, internal etc
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = CRM(**resource_dict)
else:
resources = CRM()
resources.add(
DNSEntry(name, ip, fqdn, endpoint_type))
# DNS Group
group = 'grp_{}_hostnames'.format(name)
dns_res_group_members = []
if resource_dict:
dns_resources = resource_dict.get('resources')
if dns_resources:
for dns_res in dns_resources:
if 'hostname' in dns_res:
dns_res_group_members.append(dns_res)
resources.group(group,
*sorted(dns_res_group_members))
self.set_local(resources=resources)
def remove_dnsha(self, name, endpoint_type):
"""Remove a DNS entry
:param name: string - Name of service
:param endpoint_type: string - Public, private, internal etc
:returns: None
"""
res_key = 'res_{}_{}_hostname'.format(
self.service_name.replace('-', '_'),
self.endpoint_type)
self.delete_resource(res_key)
def get_remote_all(self, key, default=None):
"""Return a list of all values presented by remote units for key"""
raise NotImplementedError
class CRM(dict):
"""
Configuration object for Pacemaker resources for the HACluster

View File

@ -11,18 +11,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import hashlib
import relations.hacluster.common
import relations.hacluster.interface_hacluster.common as common
from charms.reactive import hook
from charms.reactive import RelationBase
from charms.reactive import scopes
from charms.reactive.helpers import data_changed
from charms.reactive.helpers import data_changed as rh_data_changed
from charmhelpers.core import hookenv
class HAClusterRequires(RelationBase):
class HAClusterRequires(RelationBase, common.ResourceManagement):
# The hacluster charm is a subordinate charm and really only works
# for a single service to the HA Cluster relation, therefore set the
# expected scope to be GLOBAL.
@ -44,232 +41,8 @@ class HAClusterRequires(RelationBase):
self.remove_state('{relation_name}.available')
self.remove_state('{relation_name}.connected')
def is_clustered(self):
"""Has the hacluster charm set clustered?
The hacluster charm sets cluster=True when it determines it is ready.
Check the relation data for clustered and force a boolean return.
:returns: boolean
"""
clustered_values = self.get_remote_all('clustered')
if clustered_values:
# There is only ever one subordinate hacluster unit
clustered = clustered_values[0]
# Future versions of hacluster will return a bool
# Current versions return a string
if type(clustered) is bool:
return clustered
elif (clustered is not None and
(clustered.lower() == 'true' or
clustered.lower() == 'yes')):
return True
return False
def bind_on(self, iface=None, mcastport=None):
relation_data = {}
if iface:
relation_data['corosync_bindiface'] = iface
if mcastport:
relation_data['corosync_mcastport'] = mcastport
if relation_data and data_changed('hacluster-bind_on', relation_data):
self.set_local(**relation_data)
self.set_remote(**relation_data)
def manage_resources(self, crm):
"""
Request for the hacluster to manage the resources defined in the
crm object.
res = CRM()
res.primitive('res_neutron_haproxy', 'lsb:haproxy',
op='monitor interval="5s"')
res.init_services('haproxy')
res.clone('cl_nova_haproxy', 'res_neutron_haproxy')
hacluster.manage_resources(crm)
:param crm: CRM() instance - Config object for Pacemaker resources
:returns: None
"""
relation_data = {
'json_{}'.format(k): json.dumps(v, sort_keys=True)
for k, v in crm.items() if v
}
if data_changed('hacluster-manage_resources', relation_data):
self.set_local(**relation_data)
self.set_remote(**relation_data)
def bind_resources(self, iface=None, mcastport=None):
"""Inform the ha subordinate about each service it should manage. The
child class specifies the services via self.ha_resources
:param iface: string - Network interface to bind to
:param mcastport: int - Multicast port corosync should use for cluster
management traffic
"""
if mcastport is None:
mcastport = 4440
resources_dict = self.get_local('resources')
self.bind_on(iface=iface, mcastport=mcastport)
if resources_dict:
resources = relations.hacluster.common.CRM(**resources_dict)
self.manage_resources(resources)
def delete_resource(self, resource_name):
resource_dict = self.get_local('resources')
if resource_dict:
resources = relations.hacluster.common.CRM(**resource_dict)
else:
resources = relations.hacluster.common.CRM()
resources.add_delete_resource(resource_name)
self.set_local(resources=resources)
def add_vip(self, name, vip, iface=None, netmask=None):
"""Add a VirtualIP object for each user specified vip to self.resources
:param name: string - Name of service
:param vip: string - Virtual IP to be managed
:param iface: string - Network interface to bind vip to
:param netmask: string - Netmask for vip
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = relations.hacluster.common.CRM(**resource_dict)
else:
resources = relations.hacluster.common.CRM()
resources.add(
relations.hacluster.common.VirtualIP(
name,
vip,
nic=iface,
cidr=netmask,))
# Vip Group
group = 'grp_{}_vips'.format(name)
vip_res_group_members = []
if resource_dict:
vip_resources = resource_dict.get('resources')
if vip_resources:
for vip_res in vip_resources:
if 'vip' in vip_res:
vip_res_group_members.append(vip_res)
resources.group(group,
*sorted(vip_res_group_members))
self.set_local(resources=resources)
def remove_vip(self, name, vip, iface=None):
"""Remove a virtual IP
:param name: string - Name of service
:param vip: string - Virtual IP
:param iface: string - Network interface vip bound to
"""
if iface:
nic_name = iface
else:
nic_name = hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7]
self.delete_resource('res_{}_{}_vip'.format(name, nic_name))
def add_init_service(self, name, service, clone=True):
"""Add a InitService object for haproxy to self.resources
:param name: string - Name of service
:param service: string - Name service uses in init system
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = relations.hacluster.common.CRM(**resource_dict)
else:
resources = relations.hacluster.common.CRM()
resources.add(
relations.hacluster.common.InitService(name, service, clone))
self.set_local(resources=resources)
def remove_init_service(self, name, service):
"""Remove an init service
:param name: string - Name of service
:param service: string - Name of service used in init system
"""
res_key = 'res_{}_{}'.format(
name.replace('-', '_'),
service.replace('-', '_'))
self.delete_resource(res_key)
def add_systemd_service(self, name, service, clone=True):
"""Add a SystemdService object to self.resources
:param name: string - Name of service
:param service: string - Name service uses in systemd
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = relations.hacluster.common.CRM(**resource_dict)
else:
resources = relations.hacluster.common.CRM()
resources.add(
relations.hacluster.common.SystemdService(name, service, clone))
self.set_local(resources=resources)
def remove_systemd_service(self, name, service):
"""Remove a systemd service
:param name: string - Name of service
:param service: string - Name of service used in systemd
"""
res_key = 'res_{}_{}'.format(
name.replace('-', '_'),
service.replace('-', '_'))
self.delete_resource(res_key)
def add_dnsha(self, name, ip, fqdn, endpoint_type):
"""Add a DNS entry to self.resources
:param name: string - Name of service
:param ip: string - IP address dns entry should resolve to
:param fqdn: string - The DNS entry name
:param endpoint_type: string - Public, private, internal etc
:returns: None
"""
resource_dict = self.get_local('resources')
if resource_dict:
resources = relations.hacluster.common.CRM(**resource_dict)
else:
resources = relations.hacluster.common.CRM()
resources.add(
relations.hacluster.common.DNSEntry(name, ip, fqdn, endpoint_type))
# DNS Group
group = 'grp_{}_hostnames'.format(name)
dns_res_group_members = []
if resource_dict:
dns_resources = resource_dict.get('resources')
if dns_resources:
for dns_res in dns_resources:
if 'hostname' in dns_res:
dns_res_group_members.append(dns_res)
resources.group(group,
*sorted(dns_res_group_members))
self.set_local(resources=resources)
def remove_dnsha(self, name, endpoint_type):
"""Remove a DNS entry
:param name: string - Name of service
:param endpoint_type: string - Public, private, internal etc
:returns: None
"""
res_key = 'res_{}_{}_hostname'.format(
self.service_name.replace('-', '_'),
self.endpoint_type)
self.delete_resource(res_key)
def data_changed(self, data_id, data, hash_type='md5'):
return rh_data_changed(data_id, data, hash_type)
def get_remote_all(self, key, default=None):
"""Return a list of all values presented by remote units for key"""

View File

@ -1,5 +1,5 @@
[tox]
envlist = pep8,py37
envlist = pep8,py3
skipsdist = True
# NOTE(beisner): Avoid build/test env pollution by not enabling sitepackages.
sitepackages = False

View File

@ -13,7 +13,7 @@
from unittest import mock
import unittest
import common
import interface_hacluster.common as common
class TestHAClusterCommonCRM(unittest.TestCase):

View File

@ -15,16 +15,17 @@ import json
from unittest import mock
import unittest
import common
import interface_hacluster.common as common
# Deal with the 'relations.hacluster.common' import in requires.py which
# is invalid in the unit tests as there is no 'relations'.
relations_mock = mock.MagicMock()
relations_mock.hacluster.common = common
relations_mock.hacluster.interface_hacluster.common = common
modules = {
'relations': relations_mock,
'relations.hacluster': mock.MagicMock(),
'relations.hacluster.common': common,
'relations.hacluster.interface_hacluster': mock.MagicMock(),
'relations.hacluster.interface_hacluster.common': common,
}
module_patcher = mock.patch.dict('sys.modules', modules)
module_patcher.start()
@ -36,7 +37,7 @@ with mock.patch('charmhelpers.core.hookenv.metadata') as _meta:
_hook_args = {}
TO_PATCH = [
'data_changed',
'rh_data_changed',
]
@ -196,7 +197,7 @@ class TestHAClusterRequires(unittest.TestCase):
"res_neutron_haproxy": ' op monitor interval="5s"'},
'json_resources': {"res_neutron_haproxy": "lsb:haproxy"}}
self.jsonify(expected)
self.data_changed.return_value = True
self.rh_data_changed.return_value = True
self.patch_kr('set_local')
self.patch_kr('set_remote')
self.cr.manage_resources(res)
@ -209,7 +210,7 @@ class TestHAClusterRequires(unittest.TestCase):
op='monitor interval="5s"')
res.init_services('haproxy')
res.clone('cl_nova_haproxy', 'res_neutron_haproxy')
self.data_changed.return_value = False
self.rh_data_changed.return_value = False
self.patch_kr('set_local')
self.patch_kr('set_remote')
self.cr.manage_resources(res)