diff --git a/common.py b/interface_hacluster/common.py similarity index 75% rename from common.py rename to interface_hacluster/common.py index d896510..54ed854 100644 --- a/common.py +++ b/interface_hacluster/common.py @@ -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 diff --git a/requires.py b/requires.py index 9b72d97..395a658 100644 --- a/requires.py +++ b/requires.py @@ -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""" diff --git a/tox.ini b/tox.ini index defb8d1..a866ef1 100644 --- a/tox.ini +++ b/tox.ini @@ -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 diff --git a/unit_tests/test_common.py b/unit_tests/test_common.py index 1319c66..399f445 100644 --- a/unit_tests/test_common.py +++ b/unit_tests/test_common.py @@ -13,7 +13,7 @@ from unittest import mock import unittest -import common +import interface_hacluster.common as common class TestHAClusterCommonCRM(unittest.TestCase): diff --git a/unit_tests/test_requires.py b/unit_tests/test_requires.py index 056f189..898365e 100644 --- a/unit_tests/test_requires.py +++ b/unit_tests/test_requires.py @@ -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)