Provide heat charm HA support

[cbjchen,r=]
This commit is contained in:
Liang Chen 2016-02-19 19:22:51 +08:00
parent 0f36fa663d
commit 1bdc87f495
11 changed files with 233 additions and 15 deletions

View File

@ -122,3 +122,37 @@ options:
order for this charm to function correctly, the privacy extension must be
disabled and a non-temporary address must be configured/available on
your network interface.
# HA configuration settings
vip:
type: string
default:
description: |
Virtual IP(s) to use to front API services in HA configuration.
.
If multiple networks are being used, a VIP should be provided for each
network, separated by spaces.
vip_iface:
type: string
default: eth0
description: |
Default network interface to use for HA vip when it cannot be automatically
determined.
vip_cidr:
type: int
default: 24
description: |
Default CIDR netmask to use for HA vip when it cannot be automatically
determined.
ha-bindiface:
type: string
default: eth0
description: |
Default network interface on which HA cluster will bind to communication
with the other members of the HA Cluster.
ha-mcastport:
type: int
default: 5959
description: |
Default multicast port number that will be used to communicate between
HA Cluster nodes.

View File

@ -0,0 +1 @@
heat_relations.py

View File

@ -0,0 +1 @@
heat_relations.py

View File

@ -0,0 +1 @@
heat_relations.py

1
hooks/ha-relation-changed Symbolic link
View File

@ -0,0 +1 @@
heat_relations.py

1
hooks/ha-relation-joined Symbolic link
View File

@ -0,0 +1 @@
heat_relations.py

View File

@ -19,7 +19,9 @@ from charmhelpers.core.hookenv import (
charm_dir,
log,
relation_ids,
relation_get,
relation_set,
local_unit,
open_port,
unit_get,
status_set,
@ -39,6 +41,19 @@ from charmhelpers.fetch import (
apt_update
)
from charmhelpers.contrib.hahelpers.cluster import (
is_elected_leader,
get_hacluster_config,
)
from charmhelpers.contrib.network.ip import (
get_iface_for_address,
get_netmask_for_address,
get_address_in_network,
get_ipv6_addr,
is_ipv6
)
from charmhelpers.contrib.openstack.utils import (
configure_installation_source,
openstack_upgrade_available,
@ -59,6 +74,7 @@ from heat_utils import (
determine_packages,
migrate_database,
register_configs,
CLUSTER_RES,
HEAT_CONF,
REQUIRED_INTERFACES,
setup_ipv6,
@ -68,6 +84,7 @@ from heat_context import (
API_PORTS,
)
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
from charmhelpers.payload.execd import execd_preinstall
hooks = Hooks()
@ -112,6 +129,11 @@ def config_changed():
CONFIGS.write_all()
configure_https()
for rid in relation_ids('cluster'):
cluster_joined(relation_id=rid)
for r_id in relation_ids('ha'):
ha_joined(relation_id=r_id)
@hooks.hook('upgrade-charm')
def upgrade_charm():
@ -140,9 +162,9 @@ def db_joined():
config('database-user'),
relation_prefix='heat')
else:
relation_set(heat_database=config('database'),
heat_username=config('database-user'),
heat_hostname=unit_get('private-address'))
relation_set(database=config('database'),
username=config('database-user'),
hostname=unit_get('private-address'))
@hooks.hook('shared-db-relation-changed')
@ -152,7 +174,15 @@ def db_changed():
log('shared-db relation incomplete. Peer not ready?')
return
CONFIGS.write(HEAT_CONF)
migrate_database()
if is_elected_leader(CLUSTER_RES):
allowed_units = relation_get('allowed_units')
if allowed_units and local_unit() in allowed_units.split():
log('Cluster leader, performing db sync')
migrate_database()
else:
log('allowed_units either not presented, or local unit '
'not in acl list: %s' % repr(allowed_units))
def configure_https():
@ -231,6 +261,100 @@ def leader_elected():
leader_set({'heat-domain-admin-passwd': pwgen(32)})
@hooks.hook('cluster-relation-joined')
def cluster_joined(relation_id=None):
for addr_type in ADDRESS_TYPES:
address = get_address_in_network(
config('os-{}-network'.format(addr_type))
)
if address:
relation_set(
relation_id=relation_id,
relation_settings={'{}-address'.format(addr_type): address}
)
if config('prefer-ipv6'):
private_addr = get_ipv6_addr(exc_list=[config('vip')])[0]
relation_set(relation_id=relation_id,
relation_settings={'private-address': private_addr})
@hooks.hook('cluster-relation-changed',
'cluster-relation-departed')
@restart_on_change(restart_map(), stopstart=True)
def cluster_changed():
CONFIGS.write_all()
@hooks.hook('ha-relation-joined')
def ha_joined(relation_id=None):
cluster_config = get_hacluster_config()
resources = {
'res_heat_haproxy': 'lsb:haproxy'
}
resource_params = {
'res_heat_haproxy': 'op monitor interval="5s"'
}
vip_group = []
for vip in cluster_config['vip'].split():
if is_ipv6(vip):
res_heat_vip = 'ocf:heartbeat:IPv6addr'
vip_params = 'ipv6addr'
else:
res_heat_vip = 'ocf:heartbeat:IPaddr2'
vip_params = 'ip'
iface = (get_iface_for_address(vip) or
config('vip_iface'))
netmask = (get_netmask_for_address(vip) or
config('vip_cidr'))
if iface is not None:
vip_key = 'res_heat_{}_vip'.format(iface)
resources[vip_key] = res_heat_vip
resource_params[vip_key] = (
'params {ip}="{vip}" cidr_netmask="{netmask}"'
' nic="{iface}"'.format(ip=vip_params,
vip=vip,
iface=iface,
netmask=netmask)
)
vip_group.append(vip_key)
if len(vip_group) >= 1:
relation_set(relation_id=relation_id,
groups={'grp_heat_vips': ' '.join(vip_group)})
init_services = {
'res_heat_haproxy': 'haproxy'
}
clones = {
'cl_heat_haproxy': 'res_heat_haproxy'
}
relation_set(relation_id=relation_id,
init_services=init_services,
corosync_bindiface=cluster_config['ha-bindiface'],
corosync_mcastport=cluster_config['ha-mcastport'],
resources=resources,
resource_params=resource_params,
clones=clones)
@hooks.hook('ha-relation-changed')
def ha_changed():
clustered = relation_get('clustered')
if not clustered or clustered in [None, 'None', '']:
log('ha_changed: hacluster subordinate not fully clustered.')
else:
log('Cluster configured, notifying other services and updating '
'keystone endpoint configuration')
for rid in relation_ids('identity-service'):
identity_joined(rid=rid)
def main():
try:
hooks.execute(sys.argv)

View File

@ -68,6 +68,8 @@ BASE_SERVICES = [
'heat-engine'
]
# Cluster resource used to determine leadership when hacluster'd
CLUSTER_RES = 'grp_heat_vips'
SVC = 'heat'
HEAT_DIR = '/etc/heat'
HEAT_CONF = '/etc/heat/heat.conf'
@ -82,8 +84,7 @@ CONFIG_FILES = OrderedDict([
(HEAT_CONF, {
'services': BASE_SERVICES,
'contexts': [context.AMQPContext(ssl_dir=HEAT_DIR),
context.SharedDBContext(relation_prefix='heat',
ssl_dir=HEAT_DIR),
context.SharedDBContext(ssl_dir=HEAT_DIR),
context.OSConfigFlagContext(),
HeatIdentityServiceContext(service=SVC, service_user=SVC),
HeatHAProxyContext(),

View File

@ -14,3 +14,9 @@ requires:
interface: rabbitmq
identity-service:
interface: keystone
ha:
interface: hacluster
scope: container
peers:
cluster:
interface: heat-ha

View File

@ -345,9 +345,9 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
relation = ['shared-db', 'mysql:shared-db']
expected = {
'private-address': u.valid_ip,
'heat_database': 'heat',
'heat_username': 'heat',
'heat_hostname': u.valid_ip
'database': 'heat',
'username': 'heat',
'hostname': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
@ -363,8 +363,8 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
expected = {
'private-address': u.valid_ip,
'db_host': u.valid_ip,
'heat_allowed_units': 'heat/0',
'heat_password': u.not_null
'allowed_units': 'heat/0',
'password': u.not_null
}
ret = u.validate_relation_data(unit, relation, expected)
@ -468,7 +468,7 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
u.log.debug('mysql:heat relation: {}'.format(mysql_rel))
db_uri = "mysql://{}:{}@{}/{}".format('heat',
mysql_rel['heat_password'],
mysql_rel['password'],
mysql_rel['db_host'],
'heat')

View File

@ -41,6 +41,13 @@ TO_PATCH = [
'execd_preinstall',
'log',
'migrate_database',
'is_elected_leader',
'relation_ids',
'relation_get',
'local_unit',
'get_hacluster_config',
'get_iface_for_address',
'get_netmask_for_address',
]
@ -91,12 +98,14 @@ class HeatRelationTests(CharmTestCase):
def test_db_joined(self):
self.unit_get.return_value = 'heat.foohost.com'
relations.db_joined()
self.relation_set.assert_called_with(heat_database='heat',
heat_username='heat',
heat_hostname='heat.foohost.com')
self.relation_set.assert_called_with(database='heat',
username='heat',
hostname='heat.foohost.com')
self.unit_get.assert_called_with('private-address')
def _shared_db_test(self, configs):
self.relation_get.return_value = 'heat/0 heat/1'
self.local_unit.return_value = 'heat/0'
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['shared-db']
configs.write = MagicMock()
@ -242,3 +251,42 @@ class HeatRelationTests(CharmTestCase):
relations.db_joined()
self.sync_db_with_multi_ipv6_addresses.assert_called_with(
'heat', 'heat', relation_prefix='heat')
@patch.object(relations, 'CONFIGS')
def test_non_leader_db_changed(self, configs):
self.is_elected_leader.return_value = False
configs.complete_contexts.return_value = []
self.relation_get.return_value = 'heat/0 heat/1'
self.local_unit.return_value = 'heat/0'
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['shared-db']
configs.write = MagicMock()
relations.db_changed()
self.assertFalse(self.migrate_database.called)
@patch.object(relations, 'CONFIGS')
def test_ha_joined(self, configs):
self.get_hacluster_config.return_value = {
'ha-bindiface': 'eth0',
'ha-mcastport': '5959',
'vip': '10.5.105.3'
}
self.get_iface_for_address.return_value = 'eth0'
self.get_netmask_for_address.return_value = '255.255.255.0'
relations.ha_joined()
expected = {
'relation_id': None,
'init_services': {'res_heat_haproxy': 'haproxy'},
'corosync_bindiface': 'eth0',
'corosync_mcastport': '5959',
'resources': {
'res_heat_haproxy': 'lsb:haproxy',
'res_heat_eth0_vip': 'ocf:heartbeat:IPaddr2'},
'resource_params': {
'res_heat_haproxy': 'op monitor interval="5s"',
'res_heat_eth0_vip': ('params ip="10.5.105.3" '
'cidr_netmask="255.255.255.0" '
'nic="eth0"')},
'clones': {'cl_heat_haproxy': 'res_heat_haproxy'}
}
self.relation_set.assert_called_with(**expected)