parent
0f36fa663d
commit
1bdc87f495
34
config.yaml
34
config.yaml
|
@ -122,3 +122,37 @@ options:
|
||||||
order for this charm to function correctly, the privacy extension must be
|
order for this charm to function correctly, the privacy extension must be
|
||||||
disabled and a non-temporary address must be configured/available on
|
disabled and a non-temporary address must be configured/available on
|
||||||
your network interface.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
heat_relations.py
|
|
@ -0,0 +1 @@
|
||||||
|
heat_relations.py
|
|
@ -0,0 +1 @@
|
||||||
|
heat_relations.py
|
|
@ -0,0 +1 @@
|
||||||
|
heat_relations.py
|
|
@ -0,0 +1 @@
|
||||||
|
heat_relations.py
|
|
@ -19,7 +19,9 @@ from charmhelpers.core.hookenv import (
|
||||||
charm_dir,
|
charm_dir,
|
||||||
log,
|
log,
|
||||||
relation_ids,
|
relation_ids,
|
||||||
|
relation_get,
|
||||||
relation_set,
|
relation_set,
|
||||||
|
local_unit,
|
||||||
open_port,
|
open_port,
|
||||||
unit_get,
|
unit_get,
|
||||||
status_set,
|
status_set,
|
||||||
|
@ -39,6 +41,19 @@ from charmhelpers.fetch import (
|
||||||
apt_update
|
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 (
|
from charmhelpers.contrib.openstack.utils import (
|
||||||
configure_installation_source,
|
configure_installation_source,
|
||||||
openstack_upgrade_available,
|
openstack_upgrade_available,
|
||||||
|
@ -59,6 +74,7 @@ from heat_utils import (
|
||||||
determine_packages,
|
determine_packages,
|
||||||
migrate_database,
|
migrate_database,
|
||||||
register_configs,
|
register_configs,
|
||||||
|
CLUSTER_RES,
|
||||||
HEAT_CONF,
|
HEAT_CONF,
|
||||||
REQUIRED_INTERFACES,
|
REQUIRED_INTERFACES,
|
||||||
setup_ipv6,
|
setup_ipv6,
|
||||||
|
@ -68,6 +84,7 @@ from heat_context import (
|
||||||
API_PORTS,
|
API_PORTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
|
||||||
from charmhelpers.payload.execd import execd_preinstall
|
from charmhelpers.payload.execd import execd_preinstall
|
||||||
|
|
||||||
hooks = Hooks()
|
hooks = Hooks()
|
||||||
|
@ -112,6 +129,11 @@ def config_changed():
|
||||||
CONFIGS.write_all()
|
CONFIGS.write_all()
|
||||||
configure_https()
|
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')
|
@hooks.hook('upgrade-charm')
|
||||||
def upgrade_charm():
|
def upgrade_charm():
|
||||||
|
@ -140,9 +162,9 @@ def db_joined():
|
||||||
config('database-user'),
|
config('database-user'),
|
||||||
relation_prefix='heat')
|
relation_prefix='heat')
|
||||||
else:
|
else:
|
||||||
relation_set(heat_database=config('database'),
|
relation_set(database=config('database'),
|
||||||
heat_username=config('database-user'),
|
username=config('database-user'),
|
||||||
heat_hostname=unit_get('private-address'))
|
hostname=unit_get('private-address'))
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook('shared-db-relation-changed')
|
@hooks.hook('shared-db-relation-changed')
|
||||||
|
@ -152,7 +174,15 @@ def db_changed():
|
||||||
log('shared-db relation incomplete. Peer not ready?')
|
log('shared-db relation incomplete. Peer not ready?')
|
||||||
return
|
return
|
||||||
CONFIGS.write(HEAT_CONF)
|
CONFIGS.write(HEAT_CONF)
|
||||||
|
|
||||||
|
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()
|
migrate_database()
|
||||||
|
else:
|
||||||
|
log('allowed_units either not presented, or local unit '
|
||||||
|
'not in acl list: %s' % repr(allowed_units))
|
||||||
|
|
||||||
|
|
||||||
def configure_https():
|
def configure_https():
|
||||||
|
@ -231,6 +261,100 @@ def leader_elected():
|
||||||
leader_set({'heat-domain-admin-passwd': pwgen(32)})
|
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():
|
def main():
|
||||||
try:
|
try:
|
||||||
hooks.execute(sys.argv)
|
hooks.execute(sys.argv)
|
||||||
|
|
|
@ -68,6 +68,8 @@ BASE_SERVICES = [
|
||||||
'heat-engine'
|
'heat-engine'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Cluster resource used to determine leadership when hacluster'd
|
||||||
|
CLUSTER_RES = 'grp_heat_vips'
|
||||||
SVC = 'heat'
|
SVC = 'heat'
|
||||||
HEAT_DIR = '/etc/heat'
|
HEAT_DIR = '/etc/heat'
|
||||||
HEAT_CONF = '/etc/heat/heat.conf'
|
HEAT_CONF = '/etc/heat/heat.conf'
|
||||||
|
@ -82,8 +84,7 @@ CONFIG_FILES = OrderedDict([
|
||||||
(HEAT_CONF, {
|
(HEAT_CONF, {
|
||||||
'services': BASE_SERVICES,
|
'services': BASE_SERVICES,
|
||||||
'contexts': [context.AMQPContext(ssl_dir=HEAT_DIR),
|
'contexts': [context.AMQPContext(ssl_dir=HEAT_DIR),
|
||||||
context.SharedDBContext(relation_prefix='heat',
|
context.SharedDBContext(ssl_dir=HEAT_DIR),
|
||||||
ssl_dir=HEAT_DIR),
|
|
||||||
context.OSConfigFlagContext(),
|
context.OSConfigFlagContext(),
|
||||||
HeatIdentityServiceContext(service=SVC, service_user=SVC),
|
HeatIdentityServiceContext(service=SVC, service_user=SVC),
|
||||||
HeatHAProxyContext(),
|
HeatHAProxyContext(),
|
||||||
|
|
|
@ -14,3 +14,9 @@ requires:
|
||||||
interface: rabbitmq
|
interface: rabbitmq
|
||||||
identity-service:
|
identity-service:
|
||||||
interface: keystone
|
interface: keystone
|
||||||
|
ha:
|
||||||
|
interface: hacluster
|
||||||
|
scope: container
|
||||||
|
peers:
|
||||||
|
cluster:
|
||||||
|
interface: heat-ha
|
||||||
|
|
|
@ -345,9 +345,9 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||||
relation = ['shared-db', 'mysql:shared-db']
|
relation = ['shared-db', 'mysql:shared-db']
|
||||||
expected = {
|
expected = {
|
||||||
'private-address': u.valid_ip,
|
'private-address': u.valid_ip,
|
||||||
'heat_database': 'heat',
|
'database': 'heat',
|
||||||
'heat_username': 'heat',
|
'username': 'heat',
|
||||||
'heat_hostname': u.valid_ip
|
'hostname': u.valid_ip
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = u.validate_relation_data(unit, relation, expected)
|
ret = u.validate_relation_data(unit, relation, expected)
|
||||||
|
@ -363,8 +363,8 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||||
expected = {
|
expected = {
|
||||||
'private-address': u.valid_ip,
|
'private-address': u.valid_ip,
|
||||||
'db_host': u.valid_ip,
|
'db_host': u.valid_ip,
|
||||||
'heat_allowed_units': 'heat/0',
|
'allowed_units': 'heat/0',
|
||||||
'heat_password': u.not_null
|
'password': u.not_null
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = u.validate_relation_data(unit, relation, expected)
|
ret = u.validate_relation_data(unit, relation, expected)
|
||||||
|
@ -468,7 +468,7 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||||
u.log.debug('mysql:heat relation: {}'.format(mysql_rel))
|
u.log.debug('mysql:heat relation: {}'.format(mysql_rel))
|
||||||
|
|
||||||
db_uri = "mysql://{}:{}@{}/{}".format('heat',
|
db_uri = "mysql://{}:{}@{}/{}".format('heat',
|
||||||
mysql_rel['heat_password'],
|
mysql_rel['password'],
|
||||||
mysql_rel['db_host'],
|
mysql_rel['db_host'],
|
||||||
'heat')
|
'heat')
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,13 @@ TO_PATCH = [
|
||||||
'execd_preinstall',
|
'execd_preinstall',
|
||||||
'log',
|
'log',
|
||||||
'migrate_database',
|
'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):
|
def test_db_joined(self):
|
||||||
self.unit_get.return_value = 'heat.foohost.com'
|
self.unit_get.return_value = 'heat.foohost.com'
|
||||||
relations.db_joined()
|
relations.db_joined()
|
||||||
self.relation_set.assert_called_with(heat_database='heat',
|
self.relation_set.assert_called_with(database='heat',
|
||||||
heat_username='heat',
|
username='heat',
|
||||||
heat_hostname='heat.foohost.com')
|
hostname='heat.foohost.com')
|
||||||
self.unit_get.assert_called_with('private-address')
|
self.unit_get.assert_called_with('private-address')
|
||||||
|
|
||||||
def _shared_db_test(self, configs):
|
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 = MagicMock()
|
||||||
configs.complete_contexts.return_value = ['shared-db']
|
configs.complete_contexts.return_value = ['shared-db']
|
||||||
configs.write = MagicMock()
|
configs.write = MagicMock()
|
||||||
|
@ -242,3 +251,42 @@ class HeatRelationTests(CharmTestCase):
|
||||||
relations.db_joined()
|
relations.db_joined()
|
||||||
self.sync_db_with_multi_ipv6_addresses.assert_called_with(
|
self.sync_db_with_multi_ipv6_addresses.assert_called_with(
|
||||||
'heat', 'heat', relation_prefix='heat')
|
'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)
|
||||||
|
|
Loading…
Reference in New Issue