[hopem,r=wolsen]
Refactor and clean-up the hacluster charm. This makes the code format and layout more consistent with the rest of the openstack charms.
This commit is contained in:
commit
21c535bd6c
@ -1,2 +1,3 @@
|
|||||||
|
revision
|
||||||
bin
|
bin
|
||||||
.coverage
|
.coverage
|
||||||
|
22
Makefile
22
Makefile
@ -2,17 +2,33 @@
|
|||||||
PYTHON := /usr/bin/env python
|
PYTHON := /usr/bin/env python
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@flake8 --exclude hooks/charmhelpers hooks unit_tests
|
@flake8 --exclude hooks/charmhelpers hooks unit_tests tests
|
||||||
@charm proof
|
@charm proof
|
||||||
|
|
||||||
unit_test:
|
unit_test:
|
||||||
@echo Starting tests...
|
@echo Starting unit tests...
|
||||||
@$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
|
@$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo Starting Amulet tests...
|
||||||
|
ifndef OS_CHARMS_AMULET_VIP
|
||||||
|
@echo "WARNING: HA tests require OS_CHARMS_AMULET_VIP set to usable vip address"
|
||||||
|
endif
|
||||||
|
# coreycb note: The -v should only be temporary until Amulet sends
|
||||||
|
# raise_status() messages to stderr:
|
||||||
|
# https://bugs.launchpad.net/amulet/+bug/1320357
|
||||||
|
@juju test -v -p AMULET_HTTP_PROXY,OS_CHARMS_AMULET_VIP --timeout 900 \
|
||||||
|
00-setup 15-basic-trusty-icehouse
|
||||||
|
|
||||||
bin/charm_helpers_sync.py:
|
bin/charm_helpers_sync.py:
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
@bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
|
@bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
|
||||||
> bin/charm_helpers_sync.py
|
> bin/charm_helpers_sync.py
|
||||||
|
|
||||||
sync: bin/charm_helpers_sync.py
|
sync: bin/charm_helpers_sync.py
|
||||||
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
|
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
|
||||||
|
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
|
||||||
|
|
||||||
|
publish: lint unit_test
|
||||||
|
bzr push lp:charms/hacluster
|
||||||
|
bzr push lp:charms/trusty/hacluster
|
||||||
|
27
TODO
27
TODO
@ -1,27 +0,0 @@
|
|||||||
HA Cluster (pacemaker/corosync) Charm
|
|
||||||
======================================
|
|
||||||
* Peer-relations
|
|
||||||
- make sure node was added to the cluster
|
|
||||||
- make sure node has been removed from the cluster (when deleting unit)
|
|
||||||
* One thing that can be done is to:
|
|
||||||
1. ha-relation-joined puts node in standby.
|
|
||||||
2. ha-relation-joined makes HA configuration
|
|
||||||
3. on hanode-relation-joined (2 or more nodes)
|
|
||||||
- services are stopped from upstart/lsb
|
|
||||||
- nodes are put in online mode
|
|
||||||
- services are loaded by cluster
|
|
||||||
- this way is not in HA until we have a second node.
|
|
||||||
* Needs to communicate the VIP to the top service
|
|
||||||
* TODO: Fix Disable upstart jobs
|
|
||||||
- sudo sh -c "echo 'manual' > /etc/init/SERVICE.override"
|
|
||||||
|
|
||||||
update-rc.d -f pacemaker remove
|
|
||||||
update-rc.d pacemaker start 50 1 2 3 4 5 . stop 01 0 6 .
|
|
||||||
|
|
||||||
TODO: Problem seems to be that peer-relation gets executed before the subordinate relation.
|
|
||||||
|
|
||||||
In that case, peer relation would have to put nodes in standby and then the subordinate relation
|
|
||||||
will have to put the nodes online and configure the services. Or probably not use it at all.
|
|
||||||
|
|
||||||
Hanode-relation puts node in standby.
|
|
||||||
ha-relation counts nodes in hanode-relation and if >2 then we online them and setup cluster.
|
|
5
charm-helpers-tests.yaml
Normal file
5
charm-helpers-tests.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
branch: lp:charm-helpers
|
||||||
|
destination: tests/charmhelpers
|
||||||
|
include:
|
||||||
|
- contrib.amulet
|
||||||
|
- contrib.openstack.amulet
|
70
config.yaml
70
config.yaml
@ -1,7 +1,23 @@
|
|||||||
options:
|
options:
|
||||||
|
debug:
|
||||||
|
type: boolean
|
||||||
|
default: False
|
||||||
|
description: Enable debug logging
|
||||||
|
prefer-ipv6:
|
||||||
|
type: boolean
|
||||||
|
default: False
|
||||||
|
description: |
|
||||||
|
If True enables IPv6 support. The charm will expect network interfaces
|
||||||
|
to be configured with an IPv6 address. If set to False (default) IPv4
|
||||||
|
is expected.
|
||||||
|
.
|
||||||
|
NOTE: these charms do not currently support IPv6 privacy extension. In
|
||||||
|
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.
|
||||||
corosync_mcastaddr:
|
corosync_mcastaddr:
|
||||||
default: 226.94.1.1
|
|
||||||
type: string
|
type: string
|
||||||
|
default: 226.94.1.1
|
||||||
description: |
|
description: |
|
||||||
Multicast IP address to use for exchanging messages over the network.
|
Multicast IP address to use for exchanging messages over the network.
|
||||||
If multiple clusters are on the same bindnetaddr network, this value
|
If multiple clusters are on the same bindnetaddr network, this value
|
||||||
@ -34,9 +50,9 @@ options:
|
|||||||
type: string
|
type: string
|
||||||
default: 'False'
|
default: 'False'
|
||||||
description: |
|
description: |
|
||||||
Enable resource fencing (aka STONITH) for every node in the cluster.
|
Enable resource fencing (aka STONITH) for every node in the cluster.
|
||||||
This requires MAAS credentials be provided and each node's power
|
This requires MAAS credentials be provided and each node's power
|
||||||
parameters are properly configured in its invenvory.
|
parameters are properly configured in its invenvory.
|
||||||
maas_url:
|
maas_url:
|
||||||
type: string
|
type: string
|
||||||
default:
|
default:
|
||||||
@ -59,16 +75,16 @@ options:
|
|||||||
type: string
|
type: string
|
||||||
default:
|
default:
|
||||||
description: |
|
description: |
|
||||||
One or more IPs, separated by space, that will be used as a saftey check
|
One or more IPs, separated by space, that will be used as a saftey check
|
||||||
for avoiding split brain situations. Nodes in the cluster will ping these
|
for avoiding split brain situations. Nodes in the cluster will ping these
|
||||||
IPs periodicaly. Node that can not ping monitor_host will not run shared
|
IPs periodicaly. Node that can not ping monitor_host will not run shared
|
||||||
resources (VIP, shared disk...).
|
resources (VIP, shared disk...).
|
||||||
monitor_interval:
|
monitor_interval:
|
||||||
type: string
|
type: string
|
||||||
default: 5s
|
default: 5s
|
||||||
description: |
|
description: |
|
||||||
Time period between checks of resource health. It consists of a number
|
Time period between checks of resource health. It consists of a number
|
||||||
and a time factor, e.g. 5s = 5 seconds. 2m = 2 minutes.
|
and a time factor, e.g. 5s = 5 seconds. 2m = 2 minutes.
|
||||||
netmtu:
|
netmtu:
|
||||||
type: int
|
type: int
|
||||||
default:
|
default:
|
||||||
@ -76,40 +92,26 @@ options:
|
|||||||
Specifies the corosync.conf network mtu. If unset, the default
|
Specifies the corosync.conf network mtu. If unset, the default
|
||||||
corosync.conf value is used (currently 1500). See 'man corosync.conf' for
|
corosync.conf value is used (currently 1500). See 'man corosync.conf' for
|
||||||
detailed information on this config option.
|
detailed information on this config option.
|
||||||
prefer-ipv6:
|
|
||||||
type: boolean
|
|
||||||
default: False
|
|
||||||
description: |
|
|
||||||
If True enables IPv6 support. The charm will expect network interfaces
|
|
||||||
to be configured with an IPv6 address. If set to False (default) IPv4
|
|
||||||
is expected.
|
|
||||||
.
|
|
||||||
NOTE: these charms do not currently support IPv6 privacy extension. In
|
|
||||||
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.
|
|
||||||
corosync_transport:
|
corosync_transport:
|
||||||
type: string
|
type: string
|
||||||
default: "multicast"
|
default: "multicast"
|
||||||
description: |
|
description: |
|
||||||
Two supported modes are multicast (udp) or unicast (udpu)
|
Two supported modes are multicast (udp) or unicast (udpu)
|
||||||
debug:
|
|
||||||
default: False
|
|
||||||
type: boolean
|
|
||||||
description: Enable debug logging
|
|
||||||
nagios_context:
|
nagios_context:
|
||||||
default: "juju"
|
default: "juju"
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Used by the nrpe-external-master subordinate charm.
|
Used by the nrpe-external-master subordinate charm.
|
||||||
A string that will be prepended to instance name to set the host name
|
A string that will be prepended to instance name to set the host name
|
||||||
in nagios. So for instance the hostname would be something like:
|
in nagios. So for instance the hostname would be something like:
|
||||||
juju-postgresql-0
|
.
|
||||||
If you're running multiple environments with the same services in them
|
juju-postgresql-0
|
||||||
this allows you to differentiate between them.
|
.
|
||||||
|
If you're running multiple environments with the same services in them
|
||||||
|
this allows you to differentiate between them.
|
||||||
nagios_servicegroups:
|
nagios_servicegroups:
|
||||||
default: ""
|
default: ""
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
A comma-separated list of nagios servicegroups.
|
A comma-separated list of nagios servicegroups.
|
||||||
If left empty, the nagios_context will be used as the servicegroup
|
If left empty, the nagios_context will be used as the servicegroup
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
#
|
|
||||||
# Copyright 2012 Canonical Ltd.
|
|
||||||
#
|
|
||||||
# Authors:
|
|
||||||
# James Page <james.page@ubuntu.com>
|
|
||||||
# Paul Collins <paul.collins@canonical.com>
|
|
||||||
#
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import socket
|
|
||||||
import fcntl
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from charmhelpers.fetch import apt_install
|
|
||||||
from charmhelpers.contrib.network import ip as utils
|
|
||||||
|
|
||||||
try:
|
|
||||||
import netifaces
|
|
||||||
except ImportError:
|
|
||||||
apt_install('python-netifaces')
|
|
||||||
import netifaces
|
|
||||||
|
|
||||||
try:
|
|
||||||
from netaddr import IPNetwork
|
|
||||||
except ImportError:
|
|
||||||
apt_install('python-netaddr', fatal=True)
|
|
||||||
from netaddr import IPNetwork
|
|
||||||
|
|
||||||
|
|
||||||
def disable_upstart_services(*services):
|
|
||||||
for service in services:
|
|
||||||
with open("/etc/init/{}.override".format(service), "w") as override:
|
|
||||||
override.write("manual")
|
|
||||||
|
|
||||||
|
|
||||||
def enable_upstart_services(*services):
|
|
||||||
for service in services:
|
|
||||||
path = '/etc/init/{}.override'.format(service)
|
|
||||||
if os.path.exists(path):
|
|
||||||
os.remove(path)
|
|
||||||
|
|
||||||
|
|
||||||
def disable_lsb_services(*services):
|
|
||||||
for service in services:
|
|
||||||
subprocess.check_call(['update-rc.d', '-f', service, 'remove'])
|
|
||||||
|
|
||||||
|
|
||||||
def enable_lsb_services(*services):
|
|
||||||
for service in services:
|
|
||||||
subprocess.check_call(['update-rc.d', '-f', service, 'defaults'])
|
|
||||||
|
|
||||||
|
|
||||||
def get_iface_ipaddr(iface):
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
return socket.inet_ntoa(fcntl.ioctl(
|
|
||||||
s.fileno(),
|
|
||||||
0x8919, # SIOCGIFADDR
|
|
||||||
struct.pack('256s', iface[:15])
|
|
||||||
)[20:24])
|
|
||||||
|
|
||||||
|
|
||||||
def get_iface_netmask(iface):
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
return socket.inet_ntoa(fcntl.ioctl(
|
|
||||||
s.fileno(),
|
|
||||||
0x891b, # SIOCGIFNETMASK
|
|
||||||
struct.pack('256s', iface[:15])
|
|
||||||
)[20:24])
|
|
||||||
|
|
||||||
|
|
||||||
def get_netmask_cidr(netmask):
|
|
||||||
netmask = netmask.split('.')
|
|
||||||
binary_str = ''
|
|
||||||
for octet in netmask:
|
|
||||||
binary_str += bin(int(octet))[2:].zfill(8)
|
|
||||||
return str(len(binary_str.rstrip('0')))
|
|
||||||
|
|
||||||
|
|
||||||
def get_network_address(iface):
|
|
||||||
if iface:
|
|
||||||
iface = str(iface)
|
|
||||||
network = "{}/{}".format(get_iface_ipaddr(iface),
|
|
||||||
get_netmask_cidr(get_iface_netmask(iface)))
|
|
||||||
ip = IPNetwork(network)
|
|
||||||
return str(ip.network)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_ipv6_network_address(iface):
|
|
||||||
# Behave in same way as ipv4 get_network_address() above if iface is None.
|
|
||||||
if not iface:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
ipv6_addr = utils.get_ipv6_addr(iface=iface)[0]
|
|
||||||
all_addrs = netifaces.ifaddresses(iface)
|
|
||||||
|
|
||||||
for addr in all_addrs[netifaces.AF_INET6]:
|
|
||||||
if ipv6_addr == addr['addr']:
|
|
||||||
network = "{}/{}".format(addr['addr'], addr['netmask'])
|
|
||||||
return str(IPNetwork(network).network)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
raise Exception("Invalid interface '%s'" % iface)
|
|
||||||
|
|
||||||
raise Exception("No valid network found for interface '%s'" % iface)
|
|
428
hooks/hooks.py
428
hooks/hooks.py
@ -1,46 +1,31 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2012 Canonical Ltd.
|
# Copyright 2015 Canonical Ltd.
|
||||||
#
|
#
|
||||||
# Authors:
|
|
||||||
# Andres Rodriguez <andres.rodriguez@canonical.com>
|
|
||||||
#
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import glob
|
import glob
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
import maas as MAAS
|
|
||||||
import pcmk
|
import pcmk
|
||||||
import hacluster
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
log,
|
log,
|
||||||
relation_get,
|
DEBUG,
|
||||||
|
INFO,
|
||||||
related_units,
|
related_units,
|
||||||
relation_ids,
|
relation_ids,
|
||||||
relation_set,
|
relation_set,
|
||||||
unit_get,
|
|
||||||
config,
|
config,
|
||||||
Hooks, UnregisteredHookError,
|
Hooks,
|
||||||
local_unit,
|
UnregisteredHookError,
|
||||||
unit_private_ip,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.host import (
|
from charmhelpers.core.host import (
|
||||||
service_start,
|
|
||||||
service_stop,
|
service_stop,
|
||||||
service_restart,
|
|
||||||
service_running,
|
service_running,
|
||||||
write_file,
|
|
||||||
mkdir,
|
mkdir,
|
||||||
file_hash,
|
|
||||||
lsb_release
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.fetch import (
|
from charmhelpers.fetch import (
|
||||||
@ -50,17 +35,29 @@ from charmhelpers.fetch import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.hahelpers.cluster import (
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
peer_ips,
|
|
||||||
peer_units,
|
peer_units,
|
||||||
oldest_peer
|
oldest_peer
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.openstack.utils import get_host_ip
|
from utils import (
|
||||||
|
get_corosync_conf,
|
||||||
|
assert_charm_supports_ipv6,
|
||||||
|
get_cluster_nodes,
|
||||||
|
parse_data,
|
||||||
|
configure_corosync,
|
||||||
|
configure_stonith,
|
||||||
|
configure_monitor_host,
|
||||||
|
configure_cluster_global,
|
||||||
|
enable_lsb_services,
|
||||||
|
disable_lsb_services,
|
||||||
|
disable_upstart_services,
|
||||||
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.charmsupport import nrpe
|
from charmhelpers.contrib.charmsupport import nrpe
|
||||||
|
|
||||||
hooks = Hooks()
|
hooks = Hooks()
|
||||||
|
|
||||||
|
PACKAGES = ['corosync', 'pacemaker', 'python-netaddr', 'ipmitool']
|
||||||
COROSYNC_CONF = '/etc/corosync/corosync.conf'
|
COROSYNC_CONF = '/etc/corosync/corosync.conf'
|
||||||
COROSYNC_DEFAULT = '/etc/default/corosync'
|
COROSYNC_DEFAULT = '/etc/default/corosync'
|
||||||
COROSYNC_AUTHKEY = '/etc/corosync/authkey'
|
COROSYNC_AUTHKEY = '/etc/corosync/authkey'
|
||||||
@ -74,6 +71,7 @@ COROSYNC_CONF_FILES = [
|
|||||||
PACKAGES = ['corosync', 'pacemaker', 'python-netaddr', 'ipmitool',
|
PACKAGES = ['corosync', 'pacemaker', 'python-netaddr', 'ipmitool',
|
||||||
'libnagios-plugin-perl']
|
'libnagios-plugin-perl']
|
||||||
SUPPORTED_TRANSPORTS = ['udp', 'udpu', 'multicast', 'unicast']
|
SUPPORTED_TRANSPORTS = ['udp', 'udpu', 'multicast', 'unicast']
|
||||||
|
DEPRECATED_TRANSPORT_VALUES = {"multicast": "udp", "unicast": "udpu"}
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook()
|
@hooks.hook()
|
||||||
@ -88,12 +86,10 @@ def install():
|
|||||||
if not os.path.isfile('/usr/lib/ocf/resource.d/ceph/rbd'):
|
if not os.path.isfile('/usr/lib/ocf/resource.d/ceph/rbd'):
|
||||||
shutil.copy('ocf/ceph/rbd', '/usr/lib/ocf/resource.d/ceph/rbd')
|
shutil.copy('ocf/ceph/rbd', '/usr/lib/ocf/resource.d/ceph/rbd')
|
||||||
|
|
||||||
_deprecated_transport_values = {"multicast": "udp", "unicast": "udpu"}
|
|
||||||
|
|
||||||
|
|
||||||
def get_transport():
|
def get_transport():
|
||||||
transport = config('corosync_transport')
|
transport = config('corosync_transport')
|
||||||
val = _deprecated_transport_values.get(transport, transport)
|
val = DEPRECATED_TRANSPORT_VALUES.get(transport, transport)
|
||||||
if val not in ['udp', 'udpu']:
|
if val not in ['udp', 'udpu']:
|
||||||
msg = ("Unsupported corosync_transport type '%s' - supported "
|
msg = ("Unsupported corosync_transport type '%s' - supported "
|
||||||
"types are: %s" % (transport, ', '.join(SUPPORTED_TRANSPORTS)))
|
"types are: %s" % (transport, ', '.join(SUPPORTED_TRANSPORTS)))
|
||||||
@ -101,100 +97,6 @@ def get_transport():
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def get_corosync_id(unit_name):
|
|
||||||
# Corosync nodeid 0 is reserved so increase all the nodeids to avoid it
|
|
||||||
off_set = 1000
|
|
||||||
return off_set + int(unit_name.split('/')[1])
|
|
||||||
|
|
||||||
|
|
||||||
def get_ha_nodes():
|
|
||||||
ha_units = peer_ips(peer_relation='hanode')
|
|
||||||
ha_units[local_unit()] = unit_private_ip()
|
|
||||||
ha_nodes = {}
|
|
||||||
for unit in ha_units:
|
|
||||||
corosync_id = get_corosync_id(unit)
|
|
||||||
ha_nodes[corosync_id] = get_host_ip(ha_units[unit])
|
|
||||||
return ha_nodes
|
|
||||||
|
|
||||||
|
|
||||||
def get_corosync_conf():
|
|
||||||
if config('prefer-ipv6'):
|
|
||||||
ip_version = 'ipv6'
|
|
||||||
bindnetaddr = hacluster.get_ipv6_network_address
|
|
||||||
else:
|
|
||||||
ip_version = 'ipv4'
|
|
||||||
bindnetaddr = hacluster.get_network_address
|
|
||||||
# NOTE(jamespage) use local charm configuration over any provided by
|
|
||||||
# principle charm
|
|
||||||
conf = {
|
|
||||||
'corosync_bindnetaddr':
|
|
||||||
bindnetaddr(config('corosync_bindiface')),
|
|
||||||
'corosync_mcastport': config('corosync_mcastport'),
|
|
||||||
'corosync_mcastaddr': config('corosync_mcastaddr'),
|
|
||||||
'ip_version': ip_version,
|
|
||||||
'ha_nodes': get_ha_nodes(),
|
|
||||||
'transport': get_transport(),
|
|
||||||
'debug': config('debug'),
|
|
||||||
}
|
|
||||||
if None not in conf.itervalues():
|
|
||||||
return conf
|
|
||||||
|
|
||||||
conf = {}
|
|
||||||
|
|
||||||
if config('netmtu'):
|
|
||||||
conf['netmtu'] = config('netmtu')
|
|
||||||
|
|
||||||
for relid in relation_ids('ha'):
|
|
||||||
for unit in related_units(relid):
|
|
||||||
bindiface = relation_get('corosync_bindiface',
|
|
||||||
unit, relid)
|
|
||||||
conf = {
|
|
||||||
'corosync_bindnetaddr': bindnetaddr(bindiface),
|
|
||||||
'corosync_mcastport': relation_get('corosync_mcastport',
|
|
||||||
unit, relid),
|
|
||||||
'corosync_mcastaddr': config('corosync_mcastaddr'),
|
|
||||||
'ip_version': ip_version,
|
|
||||||
'ha_nodes': get_ha_nodes(),
|
|
||||||
'transport': get_transport(),
|
|
||||||
'debug': config('debug'),
|
|
||||||
}
|
|
||||||
|
|
||||||
if config('prefer-ipv6'):
|
|
||||||
conf['nodeid'] = get_corosync_id(local_unit())
|
|
||||||
if None not in conf.itervalues():
|
|
||||||
return conf
|
|
||||||
missing = [k for k, v in conf.iteritems() if v is None]
|
|
||||||
log('Missing required configuration: %s' % missing)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def emit_corosync_conf():
|
|
||||||
corosync_conf_context = get_corosync_conf()
|
|
||||||
if corosync_conf_context:
|
|
||||||
write_file(path=COROSYNC_CONF,
|
|
||||||
content=render_template('corosync.conf',
|
|
||||||
corosync_conf_context))
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def emit_base_conf():
|
|
||||||
corosync_default_context = {'corosync_enabled': 'yes'}
|
|
||||||
write_file(path=COROSYNC_DEFAULT,
|
|
||||||
content=render_template('corosync',
|
|
||||||
corosync_default_context))
|
|
||||||
|
|
||||||
corosync_key = config('corosync_key')
|
|
||||||
if corosync_key:
|
|
||||||
write_file(path=COROSYNC_AUTHKEY,
|
|
||||||
content=b64decode(corosync_key),
|
|
||||||
perms=0o400)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook()
|
@hooks.hook()
|
||||||
def config_changed():
|
def config_changed():
|
||||||
if config('prefer-ipv6'):
|
if config('prefer-ipv6'):
|
||||||
@ -202,10 +104,9 @@ def config_changed():
|
|||||||
|
|
||||||
corosync_key = config('corosync_key')
|
corosync_key = config('corosync_key')
|
||||||
if not corosync_key:
|
if not corosync_key:
|
||||||
log('CRITICAL',
|
raise Exception('No Corosync key supplied, cannot proceed')
|
||||||
'No Corosync key supplied, cannot proceed')
|
|
||||||
sys.exit(1)
|
enable_lsb_services('pacemaker')
|
||||||
hacluster.enable_lsb_services('pacemaker')
|
|
||||||
|
|
||||||
if configure_corosync():
|
if configure_corosync():
|
||||||
pcmk.wait_for_pcmk()
|
pcmk.wait_for_pcmk()
|
||||||
@ -223,122 +124,31 @@ def upgrade_charm():
|
|||||||
update_nrpe_config()
|
update_nrpe_config()
|
||||||
|
|
||||||
|
|
||||||
def restart_corosync():
|
|
||||||
if service_running("pacemaker"):
|
|
||||||
service_stop("pacemaker")
|
|
||||||
service_restart("corosync")
|
|
||||||
service_start("pacemaker")
|
|
||||||
|
|
||||||
|
|
||||||
def restart_corosync_on_change():
|
|
||||||
'''Simple decorator to restart corosync if any of its config changes'''
|
|
||||||
def wrap(f):
|
|
||||||
def wrapped_f(*args):
|
|
||||||
checksums = {}
|
|
||||||
for path in COROSYNC_CONF_FILES:
|
|
||||||
checksums[path] = file_hash(path)
|
|
||||||
return_data = f(*args)
|
|
||||||
# NOTE: this assumes that this call is always done around
|
|
||||||
# configure_corosync, which returns true if configuration
|
|
||||||
# files where actually generated
|
|
||||||
if return_data:
|
|
||||||
for path in COROSYNC_CONF_FILES:
|
|
||||||
if checksums[path] != file_hash(path):
|
|
||||||
restart_corosync()
|
|
||||||
break
|
|
||||||
return return_data
|
|
||||||
return wrapped_f
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
@restart_corosync_on_change()
|
|
||||||
def configure_corosync():
|
|
||||||
log('Configuring and (maybe) restarting corosync')
|
|
||||||
return emit_base_conf() and emit_corosync_conf()
|
|
||||||
|
|
||||||
|
|
||||||
def configure_monitor_host():
|
|
||||||
'''Configure extra monitor host for better network failure detection'''
|
|
||||||
log('Checking monitor host configuration')
|
|
||||||
monitor_host = config('monitor_host')
|
|
||||||
if monitor_host:
|
|
||||||
if not pcmk.crm_opt_exists('ping'):
|
|
||||||
log('Implementing monitor host'
|
|
||||||
' configuration (host: %s)' % monitor_host)
|
|
||||||
monitor_interval = config('monitor_interval')
|
|
||||||
cmd = 'crm -w -F configure primitive ping' \
|
|
||||||
' ocf:pacemaker:ping params host_list="%s"' \
|
|
||||||
' multiplier="100" op monitor interval="%s"' %\
|
|
||||||
(monitor_host, monitor_interval)
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
cmd = 'crm -w -F configure clone cl_ping ping' \
|
|
||||||
' meta interleave="true"'
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
else:
|
|
||||||
log('Reconfiguring monitor host'
|
|
||||||
' configuration (host: %s)' % monitor_host)
|
|
||||||
cmd = 'crm -w -F resource param ping set host_list="%s"' %\
|
|
||||||
monitor_host
|
|
||||||
else:
|
|
||||||
if pcmk.crm_opt_exists('ping'):
|
|
||||||
log('Disabling monitor host configuration')
|
|
||||||
pcmk.commit('crm -w -F resource stop ping')
|
|
||||||
pcmk.commit('crm -w -F configure delete ping')
|
|
||||||
|
|
||||||
|
|
||||||
def configure_cluster_global():
|
|
||||||
'''Configure global cluster options'''
|
|
||||||
log('Applying global cluster configuration')
|
|
||||||
if int(config('cluster_count')) >= 3:
|
|
||||||
# NOTE(jamespage) if 3 or more nodes, then quorum can be
|
|
||||||
# managed effectively, so stop if quorum lost
|
|
||||||
log('Configuring no-quorum-policy to stop')
|
|
||||||
cmd = "crm configure property no-quorum-policy=stop"
|
|
||||||
else:
|
|
||||||
# NOTE(jamespage) if less that 3 nodes, quorum not possible
|
|
||||||
# so ignore
|
|
||||||
log('Configuring no-quorum-policy to ignore')
|
|
||||||
cmd = "crm configure property no-quorum-policy=ignore"
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
|
|
||||||
cmd = 'crm configure rsc_defaults $id="rsc-options"' \
|
|
||||||
' resource-stickiness="100"'
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_data(relid, unit, key):
|
|
||||||
'''Simple helper to ast parse relation data'''
|
|
||||||
data = relation_get(key, unit, relid)
|
|
||||||
if data:
|
|
||||||
return ast.literal_eval(data)
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook('ha-relation-joined',
|
@hooks.hook('ha-relation-joined',
|
||||||
'ha-relation-changed',
|
'ha-relation-changed',
|
||||||
'hanode-relation-joined',
|
'hanode-relation-joined',
|
||||||
'hanode-relation-changed')
|
'hanode-relation-changed')
|
||||||
def configure_principle_cluster_resources():
|
def ha_relation_changed():
|
||||||
# Check that we are related to a principle and that
|
# Check that we are related to a principle and that
|
||||||
# it has already provided the required corosync configuration
|
# it has already provided the required corosync configuration
|
||||||
if not get_corosync_conf():
|
if not get_corosync_conf():
|
||||||
log('Unable to configure corosync right now, deferring configuration')
|
log('Unable to configure corosync right now, deferring configuration',
|
||||||
|
level=INFO)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if relation_ids('hanode'):
|
||||||
|
log('Ready to form cluster - informing peers', level=DEBUG)
|
||||||
|
relation_set(relation_id=relation_ids('hanode')[0], ready=True)
|
||||||
else:
|
else:
|
||||||
if relation_ids('hanode'):
|
log('Ready to form cluster, but not related to peers just yet',
|
||||||
log('Ready to form cluster - informing peers')
|
level=INFO)
|
||||||
relation_set(relation_id=relation_ids('hanode')[0],
|
return
|
||||||
ready=True)
|
|
||||||
else:
|
|
||||||
log('Ready to form cluster, but not related to peers just yet')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check that there's enough nodes in order to perform the
|
# Check that there's enough nodes in order to perform the
|
||||||
# configuration of the HA cluster
|
# configuration of the HA cluster
|
||||||
if (len(get_cluster_nodes()) <
|
if len(get_cluster_nodes()) < int(config('cluster_count')):
|
||||||
int(config('cluster_count'))):
|
log('Not enough nodes in cluster, deferring configuration',
|
||||||
log('Not enough nodes in cluster, deferring configuration')
|
level=INFO)
|
||||||
return
|
return
|
||||||
|
|
||||||
relids = relation_ids('ha')
|
relids = relation_ids('ha')
|
||||||
@ -347,11 +157,13 @@ def configure_principle_cluster_resources():
|
|||||||
relid = relids[0]
|
relid = relids[0]
|
||||||
units = related_units(relid)
|
units = related_units(relid)
|
||||||
if len(units) < 1:
|
if len(units) < 1:
|
||||||
log('No principle unit found, deferring configuration')
|
log('No principle unit found, deferring configuration',
|
||||||
|
level=INFO)
|
||||||
return
|
return
|
||||||
|
|
||||||
unit = units[0]
|
unit = units[0]
|
||||||
log('Parsing cluster configuration'
|
log('Parsing cluster configuration using rid: %s, unit: %s' %
|
||||||
' using rid: {}, unit: {}'.format(relid, unit))
|
(relid, unit), level=DEBUG)
|
||||||
resources = parse_data(relid, unit, 'resources')
|
resources = parse_data(relid, unit, 'resources')
|
||||||
delete_resources = parse_data(relid, unit, 'delete_resources')
|
delete_resources = parse_data(relid, unit, 'delete_resources')
|
||||||
resource_params = parse_data(relid, unit, 'resource_params')
|
resource_params = parse_data(relid, unit, 'resource_params')
|
||||||
@ -363,7 +175,7 @@ def configure_principle_cluster_resources():
|
|||||||
locations = parse_data(relid, unit, 'locations')
|
locations = parse_data(relid, unit, 'locations')
|
||||||
init_services = parse_data(relid, unit, 'init_services')
|
init_services = parse_data(relid, unit, 'init_services')
|
||||||
else:
|
else:
|
||||||
log('Related to {} ha services'.format(len(relids)))
|
log('Related to %s ha services' % (len(relids)), level=DEBUG)
|
||||||
return
|
return
|
||||||
|
|
||||||
if True in [ra.startswith('ocf:openstack')
|
if True in [ra.startswith('ocf:openstack')
|
||||||
@ -384,27 +196,26 @@ def configure_principle_cluster_resources():
|
|||||||
# Only configure the cluster resources
|
# Only configure the cluster resources
|
||||||
# from the oldest peer unit.
|
# from the oldest peer unit.
|
||||||
if oldest_peer(peer_units()):
|
if oldest_peer(peer_units()):
|
||||||
log('Deleting Resources')
|
log('Deleting Resources' % (delete_resources), level=DEBUG)
|
||||||
log(delete_resources)
|
|
||||||
for res_name in delete_resources:
|
for res_name in delete_resources:
|
||||||
if pcmk.crm_opt_exists(res_name):
|
if pcmk.crm_opt_exists(res_name):
|
||||||
log('Stopping and deleting resource %s' % res_name)
|
log('Stopping and deleting resource %s' % res_name,
|
||||||
|
level=DEBUG)
|
||||||
if pcmk.crm_res_running(res_name):
|
if pcmk.crm_res_running(res_name):
|
||||||
pcmk.commit('crm -w -F resource stop %s' % res_name)
|
pcmk.commit('crm -w -F resource stop %s' % res_name)
|
||||||
pcmk.commit('crm -w -F configure delete %s' % res_name)
|
pcmk.commit('crm -w -F configure delete %s' % res_name)
|
||||||
|
|
||||||
log('Configuring Resources')
|
log('Configuring Resources: %s' % (resources), level=DEBUG)
|
||||||
log(resources)
|
|
||||||
for res_name, res_type in resources.iteritems():
|
for res_name, res_type in resources.iteritems():
|
||||||
# disable the service we are going to put in HA
|
# disable the service we are going to put in HA
|
||||||
if res_type.split(':')[0] == "lsb":
|
if res_type.split(':')[0] == "lsb":
|
||||||
hacluster.disable_lsb_services(res_type.split(':')[1])
|
disable_lsb_services(res_type.split(':')[1])
|
||||||
if service_running(res_type.split(':')[1]):
|
if service_running(res_type.split(':')[1]):
|
||||||
service_stop(res_type.split(':')[1])
|
service_stop(res_type.split(':')[1])
|
||||||
elif (len(init_services) != 0 and
|
elif (len(init_services) != 0 and
|
||||||
res_name in init_services and
|
res_name in init_services and
|
||||||
init_services[res_name]):
|
init_services[res_name]):
|
||||||
hacluster.disable_upstart_services(init_services[res_name])
|
disable_upstart_services(init_services[res_name])
|
||||||
if service_running(init_services[res_name]):
|
if service_running(init_services[res_name]):
|
||||||
service_stop(init_services[res_name])
|
service_stop(init_services[res_name])
|
||||||
# Put the services in HA, if not already done so
|
# Put the services in HA, if not already done so
|
||||||
@ -414,69 +225,62 @@ def configure_principle_cluster_resources():
|
|||||||
cmd = 'crm -w -F configure primitive %s %s' % (res_name,
|
cmd = 'crm -w -F configure primitive %s %s' % (res_name,
|
||||||
res_type)
|
res_type)
|
||||||
else:
|
else:
|
||||||
cmd = 'crm -w -F configure primitive %s %s %s' % \
|
cmd = ('crm -w -F configure primitive %s %s %s' %
|
||||||
(res_name,
|
(res_name, res_type, resource_params[res_name]))
|
||||||
res_type,
|
|
||||||
resource_params[res_name])
|
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
if config('monitor_host'):
|
if config('monitor_host'):
|
||||||
cmd = 'crm -F configure location Ping-%s %s rule' \
|
cmd = ('crm -F configure location Ping-%s %s rule '
|
||||||
' -inf: pingd lte 0' % (res_name, res_name)
|
'-inf: pingd lte 0' % (res_name, res_name))
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
|
|
||||||
log('Configuring Groups')
|
log('Configuring Groups: %s' % (groups), level=DEBUG)
|
||||||
log(groups)
|
|
||||||
for grp_name, grp_params in groups.iteritems():
|
for grp_name, grp_params in groups.iteritems():
|
||||||
if not pcmk.crm_opt_exists(grp_name):
|
if not pcmk.crm_opt_exists(grp_name):
|
||||||
cmd = 'crm -w -F configure group %s %s' % (grp_name,
|
cmd = ('crm -w -F configure group %s %s' %
|
||||||
grp_params)
|
(grp_name, grp_params))
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
log('Configuring Master/Slave (ms)')
|
log('Configuring Master/Slave (ms): %s' % (ms), level=DEBUG)
|
||||||
log(ms)
|
|
||||||
for ms_name, ms_params in ms.iteritems():
|
for ms_name, ms_params in ms.iteritems():
|
||||||
if not pcmk.crm_opt_exists(ms_name):
|
if not pcmk.crm_opt_exists(ms_name):
|
||||||
cmd = 'crm -w -F configure ms %s %s' % (ms_name, ms_params)
|
cmd = 'crm -w -F configure ms %s %s' % (ms_name, ms_params)
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
log('Configuring Orders')
|
log('Configuring Orders: %s' % (orders), level=DEBUG)
|
||||||
log(orders)
|
|
||||||
for ord_name, ord_params in orders.iteritems():
|
for ord_name, ord_params in orders.iteritems():
|
||||||
if not pcmk.crm_opt_exists(ord_name):
|
if not pcmk.crm_opt_exists(ord_name):
|
||||||
cmd = 'crm -w -F configure order %s %s' % (ord_name,
|
cmd = 'crm -w -F configure order %s %s' % (ord_name,
|
||||||
ord_params)
|
ord_params)
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
log('Configuring Colocations')
|
log('Configuring Colocations: %s' % colocations, level=DEBUG)
|
||||||
log(colocations)
|
|
||||||
for col_name, col_params in colocations.iteritems():
|
for col_name, col_params in colocations.iteritems():
|
||||||
if not pcmk.crm_opt_exists(col_name):
|
if not pcmk.crm_opt_exists(col_name):
|
||||||
cmd = 'crm -w -F configure colocation %s %s' % (col_name,
|
cmd = 'crm -w -F configure colocation %s %s' % (col_name,
|
||||||
col_params)
|
col_params)
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
log('Configuring Clones')
|
log('Configuring Clones: %s' % clones, level=DEBUG)
|
||||||
log(clones)
|
|
||||||
for cln_name, cln_params in clones.iteritems():
|
for cln_name, cln_params in clones.iteritems():
|
||||||
if not pcmk.crm_opt_exists(cln_name):
|
if not pcmk.crm_opt_exists(cln_name):
|
||||||
cmd = 'crm -w -F configure clone %s %s' % (cln_name,
|
cmd = 'crm -w -F configure clone %s %s' % (cln_name,
|
||||||
cln_params)
|
cln_params)
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
log('Configuring Locations')
|
log('Configuring Locations: %s' % locations, level=DEBUG)
|
||||||
log(locations)
|
|
||||||
for loc_name, loc_params in locations.iteritems():
|
for loc_name, loc_params in locations.iteritems():
|
||||||
if not pcmk.crm_opt_exists(loc_name):
|
if not pcmk.crm_opt_exists(loc_name):
|
||||||
cmd = 'crm -w -F configure location %s %s' % (loc_name,
|
cmd = 'crm -w -F configure location %s %s' % (loc_name,
|
||||||
loc_params)
|
loc_params)
|
||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
log('%s' % cmd)
|
log('%s' % cmd, level=DEBUG)
|
||||||
|
|
||||||
for res_name, res_type in resources.iteritems():
|
for res_name, res_type in resources.iteritems():
|
||||||
if len(init_services) != 0 and res_name in init_services:
|
if len(init_services) != 0 and res_name in init_services:
|
||||||
@ -504,88 +308,7 @@ def configure_principle_cluster_resources():
|
|||||||
pcmk.commit(cmd)
|
pcmk.commit(cmd)
|
||||||
|
|
||||||
for rel_id in relation_ids('ha'):
|
for rel_id in relation_ids('ha'):
|
||||||
relation_set(relation_id=rel_id,
|
relation_set(relation_id=rel_id, clustered="yes")
|
||||||
clustered="yes")
|
|
||||||
|
|
||||||
|
|
||||||
def configure_stonith():
|
|
||||||
if config('stonith_enabled') not in ['true', 'True', True]:
|
|
||||||
log('Disabling STONITH')
|
|
||||||
cmd = "crm configure property stonith-enabled=false"
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
else:
|
|
||||||
log('Enabling STONITH for all nodes in cluster.')
|
|
||||||
# configure stontih resources for all nodes in cluster.
|
|
||||||
# note: this is totally provider dependent and requires
|
|
||||||
# access to the MAAS API endpoint, using endpoint and credentials
|
|
||||||
# set in config.
|
|
||||||
url = config('maas_url')
|
|
||||||
creds = config('maas_credentials')
|
|
||||||
if None in [url, creds]:
|
|
||||||
log('maas_url and maas_credentials must be set'
|
|
||||||
' in config to enable STONITH.')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
maas = MAAS.MAASHelper(url, creds)
|
|
||||||
nodes = maas.list_nodes()
|
|
||||||
if not nodes:
|
|
||||||
log('Could not obtain node inventory from '
|
|
||||||
'MAAS @ %s.' % url)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
cluster_nodes = pcmk.list_nodes()
|
|
||||||
for node in cluster_nodes:
|
|
||||||
rsc, constraint = pcmk.maas_stonith_primitive(nodes, node)
|
|
||||||
if not rsc:
|
|
||||||
log('Failed to determine STONITH primitive for node'
|
|
||||||
' %s' % node)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
rsc_name = str(rsc).split(' ')[1]
|
|
||||||
if not pcmk.is_resource_present(rsc_name):
|
|
||||||
log('Creating new STONITH primitive %s.' %
|
|
||||||
rsc_name)
|
|
||||||
cmd = 'crm -F configure %s' % rsc
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
if constraint:
|
|
||||||
cmd = 'crm -F configure %s' % constraint
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
else:
|
|
||||||
log('STONITH primitive already exists '
|
|
||||||
'for node.')
|
|
||||||
|
|
||||||
cmd = "crm configure property stonith-enabled=true"
|
|
||||||
pcmk.commit(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cluster_nodes():
|
|
||||||
hosts = []
|
|
||||||
hosts.append(unit_get('private-address'))
|
|
||||||
for relid in relation_ids('hanode'):
|
|
||||||
for unit in related_units(relid):
|
|
||||||
if relation_get('ready',
|
|
||||||
rid=relid,
|
|
||||||
unit=unit):
|
|
||||||
hosts.append(relation_get('private-address',
|
|
||||||
unit, relid))
|
|
||||||
hosts.sort()
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
TEMPLATES_DIR = 'templates'
|
|
||||||
|
|
||||||
try:
|
|
||||||
import jinja2
|
|
||||||
except ImportError:
|
|
||||||
apt_install('python-jinja2', fatal=True)
|
|
||||||
import jinja2
|
|
||||||
|
|
||||||
|
|
||||||
def render_template(template_name, context, template_dir=TEMPLATES_DIR):
|
|
||||||
templates = jinja2.Environment(
|
|
||||||
loader=jinja2.FileSystemLoader(template_dir)
|
|
||||||
)
|
|
||||||
template = templates.get_template(template_name)
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook()
|
@hooks.hook()
|
||||||
@ -595,13 +318,6 @@ def stop():
|
|||||||
apt_purge(['corosync', 'pacemaker'], fatal=True)
|
apt_purge(['corosync', 'pacemaker'], fatal=True)
|
||||||
|
|
||||||
|
|
||||||
def assert_charm_supports_ipv6():
|
|
||||||
"""Check whether we are able to support charms ipv6."""
|
|
||||||
if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty":
|
|
||||||
raise Exception("IPv6 is not supported in the charms for Ubuntu "
|
|
||||||
"versions less than Trusty 14.04")
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook('nrpe-external-master-relation-joined',
|
@hooks.hook('nrpe-external-master-relation-joined',
|
||||||
'nrpe-external-master-relation-changed')
|
'nrpe-external-master-relation-changed')
|
||||||
def update_nrpe_config():
|
def update_nrpe_config():
|
||||||
@ -659,4 +375,4 @@ if __name__ == '__main__':
|
|||||||
try:
|
try:
|
||||||
hooks.execute(sys.argv)
|
hooks.execute(sys.argv)
|
||||||
except UnregisteredHookError as e:
|
except UnregisteredHookError as e:
|
||||||
log('Unknown hook {} - skipping.'.format(e))
|
log('Unknown hook {} - skipping.'.format(e), level=DEBUG)
|
||||||
|
@ -4,7 +4,10 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from charmhelpers.fetch import apt_install
|
from charmhelpers.fetch import apt_install
|
||||||
from charmhelpers.core.hookenv import log, ERROR
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
MAAS_STABLE_PPA = 'ppa:maas-maintainers/stable '
|
MAAS_STABLE_PPA = 'ppa:maas-maintainers/stable '
|
||||||
MAAS_PROFILE_NAME = 'maas-juju-hacluster'
|
MAAS_PROFILE_NAME = 'maas-juju-hacluster'
|
||||||
@ -18,10 +21,10 @@ class MAASHelper(object):
|
|||||||
self.install_maas_cli()
|
self.install_maas_cli()
|
||||||
|
|
||||||
def install_maas_cli(self):
|
def install_maas_cli(self):
|
||||||
'''
|
"""Ensure maas-cli is installed
|
||||||
Ensure maas-cli is installed. Fallback to MAAS stable PPA when
|
|
||||||
needed.
|
Fallback to MAAS stable PPA when needed.
|
||||||
'''
|
"""
|
||||||
apt.init()
|
apt.init()
|
||||||
cache = apt.Cache()
|
cache = apt.Cache()
|
||||||
|
|
||||||
@ -59,5 +62,6 @@ class MAASHelper(object):
|
|||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
log('Could not get node inventory from MAAS.', ERROR)
|
log('Could not get node inventory from MAAS.', ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
return json.loads(out)
|
return json.loads(out)
|
||||||
|
@ -2,7 +2,10 @@ import commands
|
|||||||
import subprocess
|
import subprocess
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from charmhelpers.core.hookenv import log, ERROR
|
from charmhelpers.core.hookenv import (
|
||||||
|
log,
|
||||||
|
ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_pcmk():
|
def wait_for_pcmk():
|
||||||
@ -21,6 +24,7 @@ def is_resource_present(resource):
|
|||||||
status = commands.getstatusoutput("crm resource status %s" % resource)[0]
|
status = commands.getstatusoutput("crm resource status %s" % resource)[0]
|
||||||
if status != 0:
|
if status != 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +33,7 @@ def standby(node=None):
|
|||||||
cmd = "crm -F node standby"
|
cmd = "crm -F node standby"
|
||||||
else:
|
else:
|
||||||
cmd = "crm -F node standby %s" % node
|
cmd = "crm -F node standby %s" % node
|
||||||
|
|
||||||
commit(cmd)
|
commit(cmd)
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +42,7 @@ def online(node=None):
|
|||||||
cmd = "crm -F node online"
|
cmd = "crm -F node online"
|
||||||
else:
|
else:
|
||||||
cmd = "crm -F node online %s" % node
|
cmd = "crm -F node online %s" % node
|
||||||
|
|
||||||
commit(cmd)
|
commit(cmd)
|
||||||
|
|
||||||
|
|
||||||
@ -44,15 +50,16 @@ def crm_opt_exists(opt_name):
|
|||||||
output = commands.getstatusoutput("crm configure show")[1]
|
output = commands.getstatusoutput("crm configure show")[1]
|
||||||
if opt_name in output:
|
if opt_name in output:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def crm_res_running(opt_name):
|
def crm_res_running(opt_name):
|
||||||
(c, output) = commands.getstatusoutput("crm resource status %s" % opt_name)
|
(_, output) = commands.getstatusoutput("crm resource status %s" % opt_name)
|
||||||
if output.startswith("resource %s is running" % opt_name):
|
if output.startswith("resource %s is running" % opt_name):
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def list_nodes():
|
def list_nodes():
|
||||||
@ -62,20 +69,21 @@ def list_nodes():
|
|||||||
for line in str(out).split('\n'):
|
for line in str(out).split('\n'):
|
||||||
if line != '':
|
if line != '':
|
||||||
nodes.append(line.split(':')[0])
|
nodes.append(line.split(':')[0])
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
def _maas_ipmi_stonith_resource(node, power_params):
|
def _maas_ipmi_stonith_resource(node, power_params):
|
||||||
rsc_name = 'res_stonith_%s' % node
|
rsc_name = 'res_stonith_%s' % node
|
||||||
rsc = 'primitive %s stonith:external/ipmi' % rsc_name
|
rsc = ('primitive %s stonith:external/ipmi params hostname=%s ipaddr=%s '
|
||||||
rsc += ' params hostname=%s ipaddr=%s userid=%s passwd=%s interface=lan' %\
|
'userid=%s passwd=%s interface=lan' %
|
||||||
(node, power_params['power_address'],
|
(rsc_name, node, power_params['power_address'],
|
||||||
power_params['power_user'], power_params['power_pass'])
|
power_params['power_user'], power_params['power_pass']))
|
||||||
|
|
||||||
# ensure ipmi stonith agents are not running on the nodes that
|
# ensure ipmi stonith agents are not running on the nodes that
|
||||||
# they manage.
|
# they manage.
|
||||||
constraint = 'location const_loc_stonith_avoid_%s %s -inf: %s' %\
|
constraint = ('location const_loc_stonith_avoid_%s %s -inf: %s' %
|
||||||
(node, rsc_name, node)
|
(node, rsc_name, node))
|
||||||
|
|
||||||
return rsc, constraint
|
return rsc, constraint
|
||||||
|
|
||||||
|
427
hooks/utils.py
Normal file
427
hooks/utils.py
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
import ast
|
||||||
|
import pcmk
|
||||||
|
import maas
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import socket
|
||||||
|
import fcntl
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
local_unit,
|
||||||
|
log,
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
relation_get,
|
||||||
|
related_units,
|
||||||
|
relation_ids,
|
||||||
|
config,
|
||||||
|
unit_private_ip,
|
||||||
|
unit_get,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.openstack.utils import get_host_ip
|
||||||
|
from charmhelpers.core.host import (
|
||||||
|
service_start,
|
||||||
|
service_stop,
|
||||||
|
service_restart,
|
||||||
|
service_running,
|
||||||
|
write_file,
|
||||||
|
file_hash,
|
||||||
|
lsb_release
|
||||||
|
)
|
||||||
|
from charmhelpers.fetch import (
|
||||||
|
apt_install,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
|
peer_ips,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.network import ip as utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
import netifaces
|
||||||
|
except ImportError:
|
||||||
|
apt_install('python-netifaces')
|
||||||
|
import netifaces
|
||||||
|
|
||||||
|
try:
|
||||||
|
from netaddr import IPNetwork
|
||||||
|
except ImportError:
|
||||||
|
apt_install('python-netaddr', fatal=True)
|
||||||
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jinja2
|
||||||
|
except ImportError:
|
||||||
|
apt_install('python-jinja2', fatal=True)
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATES_DIR = 'templates'
|
||||||
|
COROSYNC_CONF = '/etc/corosync/corosync.conf'
|
||||||
|
COROSYNC_DEFAULT = '/etc/default/corosync'
|
||||||
|
COROSYNC_AUTHKEY = '/etc/corosync/authkey'
|
||||||
|
COROSYNC_CONF_FILES = [
|
||||||
|
COROSYNC_DEFAULT,
|
||||||
|
COROSYNC_AUTHKEY,
|
||||||
|
COROSYNC_CONF
|
||||||
|
]
|
||||||
|
SUPPORTED_TRANSPORTS = ['udp', 'udpu', 'multicast', 'unicast']
|
||||||
|
|
||||||
|
|
||||||
|
def disable_upstart_services(*services):
|
||||||
|
for service in services:
|
||||||
|
with open("/etc/init/{}.override".format(service), "w") as override:
|
||||||
|
override.write("manual")
|
||||||
|
|
||||||
|
|
||||||
|
def enable_upstart_services(*services):
|
||||||
|
for service in services:
|
||||||
|
path = '/etc/init/{}.override'.format(service)
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_lsb_services(*services):
|
||||||
|
for service in services:
|
||||||
|
subprocess.check_call(['update-rc.d', '-f', service, 'remove'])
|
||||||
|
|
||||||
|
|
||||||
|
def enable_lsb_services(*services):
|
||||||
|
for service in services:
|
||||||
|
subprocess.check_call(['update-rc.d', '-f', service, 'defaults'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_iface_ipaddr(iface):
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
return socket.inet_ntoa(fcntl.ioctl(
|
||||||
|
s.fileno(),
|
||||||
|
0x8919, # SIOCGIFADDR
|
||||||
|
struct.pack('256s', iface[:15])
|
||||||
|
)[20:24])
|
||||||
|
|
||||||
|
|
||||||
|
def get_iface_netmask(iface):
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
return socket.inet_ntoa(fcntl.ioctl(
|
||||||
|
s.fileno(),
|
||||||
|
0x891b, # SIOCGIFNETMASK
|
||||||
|
struct.pack('256s', iface[:15])
|
||||||
|
)[20:24])
|
||||||
|
|
||||||
|
|
||||||
|
def get_netmask_cidr(netmask):
|
||||||
|
netmask = netmask.split('.')
|
||||||
|
binary_str = ''
|
||||||
|
for octet in netmask:
|
||||||
|
binary_str += bin(int(octet))[2:].zfill(8)
|
||||||
|
return str(len(binary_str.rstrip('0')))
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_address(iface):
|
||||||
|
if iface:
|
||||||
|
iface = str(iface)
|
||||||
|
network = "{}/{}".format(get_iface_ipaddr(iface),
|
||||||
|
get_netmask_cidr(get_iface_netmask(iface)))
|
||||||
|
ip = IPNetwork(network)
|
||||||
|
return str(ip.network)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ipv6_network_address(iface):
|
||||||
|
# Behave in same way as ipv4 get_network_address() above if iface is None.
|
||||||
|
if not iface:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ipv6_addr = utils.get_ipv6_addr(iface=iface)[0]
|
||||||
|
all_addrs = netifaces.ifaddresses(iface)
|
||||||
|
|
||||||
|
for addr in all_addrs[netifaces.AF_INET6]:
|
||||||
|
if ipv6_addr == addr['addr']:
|
||||||
|
network = "{}/{}".format(addr['addr'], addr['netmask'])
|
||||||
|
return str(IPNetwork(network).network)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
raise Exception("Invalid interface '%s'" % iface)
|
||||||
|
|
||||||
|
raise Exception("No valid network found for interface '%s'" % iface)
|
||||||
|
|
||||||
|
|
||||||
|
def get_corosync_id(unit_name):
|
||||||
|
# Corosync nodeid 0 is reserved so increase all the nodeids to avoid it
|
||||||
|
off_set = 1000
|
||||||
|
return off_set + int(unit_name.split('/')[1])
|
||||||
|
|
||||||
|
|
||||||
|
def get_ha_nodes():
|
||||||
|
ha_units = peer_ips(peer_relation='hanode')
|
||||||
|
ha_units[local_unit()] = unit_private_ip()
|
||||||
|
ha_nodes = {}
|
||||||
|
for unit in ha_units:
|
||||||
|
corosync_id = get_corosync_id(unit)
|
||||||
|
ha_nodes[corosync_id] = get_host_ip(ha_units[unit])
|
||||||
|
return ha_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def get_corosync_conf():
|
||||||
|
if config('prefer-ipv6'):
|
||||||
|
ip_version = 'ipv6'
|
||||||
|
bindnetaddr = get_ipv6_network_address
|
||||||
|
else:
|
||||||
|
ip_version = 'ipv4'
|
||||||
|
bindnetaddr = get_network_address
|
||||||
|
|
||||||
|
# NOTE(jamespage) use local charm configuration over any provided by
|
||||||
|
# principle charm
|
||||||
|
conf = {
|
||||||
|
'corosync_bindnetaddr':
|
||||||
|
bindnetaddr(config('corosync_bindiface')),
|
||||||
|
'corosync_mcastport': config('corosync_mcastport'),
|
||||||
|
'corosync_mcastaddr': config('corosync_mcastaddr'),
|
||||||
|
'ip_version': ip_version,
|
||||||
|
'ha_nodes': get_ha_nodes(),
|
||||||
|
'transport': get_transport(),
|
||||||
|
'debug': config('debug'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if None not in conf.itervalues():
|
||||||
|
return conf
|
||||||
|
|
||||||
|
conf = {}
|
||||||
|
|
||||||
|
if config('netmtu'):
|
||||||
|
conf['netmtu'] = config('netmtu')
|
||||||
|
|
||||||
|
for relid in relation_ids('ha'):
|
||||||
|
for unit in related_units(relid):
|
||||||
|
bindiface = relation_get('corosync_bindiface',
|
||||||
|
unit, relid)
|
||||||
|
conf = {
|
||||||
|
'corosync_bindnetaddr': bindnetaddr(bindiface),
|
||||||
|
'corosync_mcastport': relation_get('corosync_mcastport',
|
||||||
|
unit, relid),
|
||||||
|
'corosync_mcastaddr': config('corosync_mcastaddr'),
|
||||||
|
'ip_version': ip_version,
|
||||||
|
'ha_nodes': get_ha_nodes(),
|
||||||
|
'transport': get_transport(),
|
||||||
|
'debug': config('debug'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config('prefer-ipv6'):
|
||||||
|
conf['nodeid'] = get_corosync_id(local_unit())
|
||||||
|
|
||||||
|
if None not in conf.itervalues():
|
||||||
|
return conf
|
||||||
|
|
||||||
|
missing = [k for k, v in conf.iteritems() if v is None]
|
||||||
|
log('Missing required configuration: %s' % missing)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def emit_corosync_conf():
|
||||||
|
corosync_conf_context = get_corosync_conf()
|
||||||
|
if corosync_conf_context:
|
||||||
|
write_file(path=COROSYNC_CONF,
|
||||||
|
content=render_template('corosync.conf',
|
||||||
|
corosync_conf_context))
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def emit_base_conf():
|
||||||
|
corosync_default_context = {'corosync_enabled': 'yes'}
|
||||||
|
write_file(path=COROSYNC_DEFAULT,
|
||||||
|
content=render_template('corosync',
|
||||||
|
corosync_default_context))
|
||||||
|
|
||||||
|
corosync_key = config('corosync_key')
|
||||||
|
if corosync_key:
|
||||||
|
write_file(path=COROSYNC_AUTHKEY,
|
||||||
|
content=b64decode(corosync_key),
|
||||||
|
perms=0o400)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def render_template(template_name, context, template_dir=TEMPLATES_DIR):
|
||||||
|
templates = jinja2.Environment(
|
||||||
|
loader=jinja2.FileSystemLoader(template_dir)
|
||||||
|
)
|
||||||
|
template = templates.get_template(template_name)
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_charm_supports_ipv6():
|
||||||
|
"""Check whether we are able to support charms ipv6."""
|
||||||
|
if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty":
|
||||||
|
raise Exception("IPv6 is not supported in the charms for Ubuntu "
|
||||||
|
"versions less than Trusty 14.04")
|
||||||
|
|
||||||
|
|
||||||
|
def get_transport():
|
||||||
|
transport = config('corosync_transport')
|
||||||
|
_deprecated_transport_values = {"multicast": "udp", "unicast": "udpu"}
|
||||||
|
val = _deprecated_transport_values.get(transport, transport)
|
||||||
|
if val not in ['udp', 'udpu']:
|
||||||
|
msg = ("Unsupported corosync_transport type '%s' - supported "
|
||||||
|
"types are: %s" % (transport, ', '.join(SUPPORTED_TRANSPORTS)))
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def get_cluster_nodes():
|
||||||
|
hosts = []
|
||||||
|
hosts.append(unit_get('private-address'))
|
||||||
|
for relid in relation_ids('hanode'):
|
||||||
|
for unit in related_units(relid):
|
||||||
|
if relation_get('ready', rid=relid, unit=unit):
|
||||||
|
hosts.append(relation_get('private-address', unit, relid))
|
||||||
|
|
||||||
|
hosts.sort()
|
||||||
|
return hosts
|
||||||
|
|
||||||
|
|
||||||
|
def parse_data(relid, unit, key):
|
||||||
|
"""Simple helper to ast parse relation data"""
|
||||||
|
data = relation_get(key, unit, relid)
|
||||||
|
if data:
|
||||||
|
return ast.literal_eval(data)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def configure_stonith():
|
||||||
|
if config('stonith_enabled') not in ['true', 'True', True]:
|
||||||
|
log('Disabling STONITH', level=INFO)
|
||||||
|
cmd = "crm configure property stonith-enabled=false"
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
else:
|
||||||
|
log('Enabling STONITH for all nodes in cluster.', level=INFO)
|
||||||
|
# configure stontih resources for all nodes in cluster.
|
||||||
|
# note: this is totally provider dependent and requires
|
||||||
|
# access to the MAAS API endpoint, using endpoint and credentials
|
||||||
|
# set in config.
|
||||||
|
url = config('maas_url')
|
||||||
|
creds = config('maas_credentials')
|
||||||
|
if None in [url, creds]:
|
||||||
|
raise Exception('maas_url and maas_credentials must be set '
|
||||||
|
'in config to enable STONITH.')
|
||||||
|
|
||||||
|
nodes = maas.MAASHelper(url, creds).list_nodes()
|
||||||
|
if not nodes:
|
||||||
|
raise Exception('Could not obtain node inventory from '
|
||||||
|
'MAAS @ %s.' % url)
|
||||||
|
|
||||||
|
cluster_nodes = pcmk.list_nodes()
|
||||||
|
for node in cluster_nodes:
|
||||||
|
rsc, constraint = pcmk.maas_stonith_primitive(nodes, node)
|
||||||
|
if not rsc:
|
||||||
|
raise Exception('Failed to determine STONITH primitive for '
|
||||||
|
'node %s' % node)
|
||||||
|
|
||||||
|
rsc_name = str(rsc).split(' ')[1]
|
||||||
|
if not pcmk.is_resource_present(rsc_name):
|
||||||
|
log('Creating new STONITH primitive %s.' % rsc_name,
|
||||||
|
level=DEBUG)
|
||||||
|
cmd = 'crm -F configure %s' % rsc
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
if constraint:
|
||||||
|
cmd = 'crm -F configure %s' % constraint
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
else:
|
||||||
|
log('STONITH primitive already exists for node.', level=DEBUG)
|
||||||
|
|
||||||
|
pcmk.commit("crm configure property stonith-enabled=true")
|
||||||
|
|
||||||
|
|
||||||
|
def configure_monitor_host():
|
||||||
|
"""Configure extra monitor host for better network failure detection"""
|
||||||
|
log('Checking monitor host configuration', level=DEBUG)
|
||||||
|
monitor_host = config('monitor_host')
|
||||||
|
if monitor_host:
|
||||||
|
if not pcmk.crm_opt_exists('ping'):
|
||||||
|
log('Implementing monitor host configuration (host: %s)' %
|
||||||
|
monitor_host, level=DEBUG)
|
||||||
|
monitor_interval = config('monitor_interval')
|
||||||
|
cmd = ('crm -w -F configure primitive ping '
|
||||||
|
'ocf:pacemaker:ping params host_list="%s" '
|
||||||
|
'multiplier="100" op monitor interval="%s" ' %
|
||||||
|
(monitor_host, monitor_interval))
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
cmd = ('crm -w -F configure clone cl_ping ping '
|
||||||
|
'meta interleave="true"')
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
else:
|
||||||
|
log('Reconfiguring monitor host configuration (host: %s)' %
|
||||||
|
monitor_host, level=DEBUG)
|
||||||
|
cmd = ('crm -w -F resource param ping set host_list="%s"' %
|
||||||
|
monitor_host)
|
||||||
|
else:
|
||||||
|
if pcmk.crm_opt_exists('ping'):
|
||||||
|
log('Disabling monitor host configuration', level=DEBUG)
|
||||||
|
pcmk.commit('crm -w -F resource stop ping')
|
||||||
|
pcmk.commit('crm -w -F configure delete ping')
|
||||||
|
|
||||||
|
|
||||||
|
def configure_cluster_global():
|
||||||
|
"""Configure global cluster options"""
|
||||||
|
log('Applying global cluster configuration', level=DEBUG)
|
||||||
|
if int(config('cluster_count')) >= 3:
|
||||||
|
# NOTE(jamespage) if 3 or more nodes, then quorum can be
|
||||||
|
# managed effectively, so stop if quorum lost
|
||||||
|
log('Configuring no-quorum-policy to stop', level=DEBUG)
|
||||||
|
cmd = "crm configure property no-quorum-policy=stop"
|
||||||
|
else:
|
||||||
|
# NOTE(jamespage) if less that 3 nodes, quorum not possible
|
||||||
|
# so ignore
|
||||||
|
log('Configuring no-quorum-policy to ignore', level=DEBUG)
|
||||||
|
cmd = "crm configure property no-quorum-policy=ignore"
|
||||||
|
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
cmd = ('crm configure rsc_defaults $id="rsc-options" '
|
||||||
|
'resource-stickiness="100"')
|
||||||
|
pcmk.commit(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def restart_corosync_on_change():
|
||||||
|
"""Simple decorator to restart corosync if any of its config changes"""
|
||||||
|
def wrap(f):
|
||||||
|
def wrapped_f(*args, **kwargs):
|
||||||
|
checksums = {}
|
||||||
|
for path in COROSYNC_CONF_FILES:
|
||||||
|
checksums[path] = file_hash(path)
|
||||||
|
return_data = f(*args, **kwargs)
|
||||||
|
# NOTE: this assumes that this call is always done around
|
||||||
|
# configure_corosync, which returns true if configuration
|
||||||
|
# files where actually generated
|
||||||
|
if return_data:
|
||||||
|
for path in COROSYNC_CONF_FILES:
|
||||||
|
if checksums[path] != file_hash(path):
|
||||||
|
restart_corosync()
|
||||||
|
break
|
||||||
|
|
||||||
|
return return_data
|
||||||
|
return wrapped_f
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
@restart_corosync_on_change()
|
||||||
|
def configure_corosync():
|
||||||
|
log('Configuring and (maybe) restarting corosync', level=DEBUG)
|
||||||
|
return emit_base_conf() and emit_corosync_conf()
|
||||||
|
|
||||||
|
|
||||||
|
def restart_corosync():
|
||||||
|
if service_running("pacemaker"):
|
||||||
|
service_stop("pacemaker")
|
||||||
|
|
||||||
|
service_restart("corosync")
|
||||||
|
service_start("pacemaker")
|
@ -2,5 +2,5 @@
|
|||||||
verbosity=2
|
verbosity=2
|
||||||
with-coverage=1
|
with-coverage=1
|
||||||
cover-erase=1
|
cover-erase=1
|
||||||
cover-package=hooks
|
cover-package=hooks,utils,pcmk,maas
|
||||||
|
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash -eux
|
||||||
|
# Install amulet packages
|
||||||
set -ex
|
sudo apt-get update --yes
|
||||||
|
|
||||||
# Check if amulet is installed before adding repository and updating apt-get.
|
|
||||||
dpkg -s amulet
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
sudo add-apt-repository -y ppa:juju/stable
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y amulet
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install any additional python packages or software here.
|
# Install any additional python packages or software here.
|
||||||
|
sudo apt-get install --yes python-amulet python-keystoneclient || true
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# This amulet test deploys the bundles.yaml file in this directory.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
import yaml
|
|
||||||
import amulet
|
|
||||||
|
|
||||||
seconds_to_wait = 600
|
|
||||||
|
|
||||||
|
|
||||||
class BundleTest(unittest.TestCase):
|
|
||||||
""" Create a class for testing the charm in the unit test framework. """
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
""" Set up an amulet deployment using the bundle. """
|
|
||||||
d = amulet.Deployment()
|
|
||||||
bundle_path = os.path.join(os.path.dirname(__file__), 'bundles.yaml')
|
|
||||||
with open(bundle_path, 'r') as bundle_file:
|
|
||||||
contents = yaml.safe_load(bundle_file)
|
|
||||||
d.load(contents)
|
|
||||||
d.setup(seconds_to_wait)
|
|
||||||
d.sentry.wait(seconds_to_wait)
|
|
||||||
cls.d = d
|
|
||||||
|
|
||||||
def test_deployed(self):
|
|
||||||
""" Test to see if the bundle deployed successfully. """
|
|
||||||
self.assertTrue(self.d.deployed)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
9
tests/15-basic-trusty-icehouse
Executable file
9
tests/15-basic-trusty-icehouse
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
"""Amulet tests on a basic hacluster deployment on trusty-icehouse."""
|
||||||
|
|
||||||
|
from basic_deployment import HAClusterBasicDeployment
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
deployment = HAClusterBasicDeployment(series='trusty')
|
||||||
|
deployment.run_tests()
|
109
tests/basic_deployment.py
Executable file
109
tests/basic_deployment.py
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import amulet
|
||||||
|
|
||||||
|
import keystoneclient.v2_0 as keystone_client
|
||||||
|
|
||||||
|
from charmhelpers.contrib.openstack.amulet.deployment import (
|
||||||
|
OpenStackAmuletDeployment
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.openstack.amulet.utils import (
|
||||||
|
OpenStackAmuletUtils,
|
||||||
|
DEBUG, # flake8: noqa
|
||||||
|
ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use DEBUG to turn on debug logging
|
||||||
|
u = OpenStackAmuletUtils(DEBUG)
|
||||||
|
seconds_to_wait = 600
|
||||||
|
|
||||||
|
|
||||||
|
class HAClusterBasicDeployment(OpenStackAmuletDeployment):
|
||||||
|
|
||||||
|
def __init__(self, series=None, openstack=None, source=None, stable=False):
|
||||||
|
"""Deploy the entire test environment."""
|
||||||
|
super(HAClusterBasicDeployment, self).__init__(series, openstack,
|
||||||
|
source, stable)
|
||||||
|
env_var = 'OS_CHARMS_AMULET_VIP'
|
||||||
|
self._vip = os.getenv(env_var, None)
|
||||||
|
if not self._vip:
|
||||||
|
amulet.raise_status(amulet.SKIP, msg="No vip provided with '%s' - "
|
||||||
|
"skipping tests" % (env_var))
|
||||||
|
|
||||||
|
self._add_services()
|
||||||
|
self._add_relations()
|
||||||
|
self._configure_services()
|
||||||
|
self._deploy()
|
||||||
|
self._initialize_tests()
|
||||||
|
|
||||||
|
def _add_services(self):
|
||||||
|
this_service = {'name': 'hacluster'}
|
||||||
|
other_services = [{'name': 'mysql'}, {'name': 'keystone', 'units': 3}]
|
||||||
|
super(HAClusterBasicDeployment, self)._add_services(this_service,
|
||||||
|
other_services)
|
||||||
|
|
||||||
|
def _add_relations(self):
|
||||||
|
relations = {'keystone:shared-db': 'mysql:shared-db',
|
||||||
|
'hacluster:ha': 'keystone:ha'}
|
||||||
|
super(HAClusterBasicDeployment, self)._add_relations(relations)
|
||||||
|
|
||||||
|
def _configure_services(self):
|
||||||
|
keystone_config = {'admin-password': 'openstack',
|
||||||
|
'admin-token': 'ubuntutesting',
|
||||||
|
'vip': self._vip}
|
||||||
|
mysql_config = {'dataset-size': '50%'}
|
||||||
|
configs = {'keystone': keystone_config,
|
||||||
|
'mysql': mysql_config}
|
||||||
|
super(HAClusterBasicDeployment, self)._configure_services(configs)
|
||||||
|
|
||||||
|
def _authenticate_keystone_admin(self, keystone_sentry, user, password,
|
||||||
|
tenant, service_ip=None):
|
||||||
|
"""Authenticates admin user with the keystone admin endpoint.
|
||||||
|
|
||||||
|
This should be factored into:L
|
||||||
|
|
||||||
|
charmhelpers.contrib.openstack.amulet.utils.OpenStackAmuletUtils
|
||||||
|
"""
|
||||||
|
if not service_ip:
|
||||||
|
unit = keystone_sentry
|
||||||
|
service_ip = unit.relation('shared-db',
|
||||||
|
'mysql:shared-db')['private-address']
|
||||||
|
|
||||||
|
ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
|
||||||
|
return keystone_client.Client(username=user, password=password,
|
||||||
|
tenant_name=tenant, auth_url=ep)
|
||||||
|
|
||||||
|
def _initialize_tests(self):
|
||||||
|
"""Perform final initialization before tests get run."""
|
||||||
|
# Access the sentries for inspecting service units
|
||||||
|
self.mysql_sentry = self.d.sentry.unit['mysql/0']
|
||||||
|
self.keystone_sentry = self.d.sentry.unit['keystone/0']
|
||||||
|
# NOTE: the hacluster unit id may not correspond with its parent unit
|
||||||
|
# id.
|
||||||
|
self.hacluster_sentry = self.d.sentry.unit['hacluster/0']
|
||||||
|
|
||||||
|
# Authenticate keystone admin
|
||||||
|
self.keystone = self._authenticate_keystone_admin(self.keystone_sentry,
|
||||||
|
user='admin',
|
||||||
|
password='openstack',
|
||||||
|
tenant='admin',
|
||||||
|
service_ip=self._vip)
|
||||||
|
|
||||||
|
# Create a demo tenant/role/user
|
||||||
|
self.demo_tenant = 'demoTenant'
|
||||||
|
self.demo_role = 'demoRole'
|
||||||
|
self.demo_user = 'demoUser'
|
||||||
|
if not u.tenant_exists(self.keystone, self.demo_tenant):
|
||||||
|
tenant = self.keystone.tenants.create(tenant_name=self.demo_tenant,
|
||||||
|
description='demo tenant',
|
||||||
|
enabled=True)
|
||||||
|
self.keystone.roles.create(name=self.demo_role)
|
||||||
|
self.keystone.users.create(name=self.demo_user, password='password',
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
email='demo@demo.com')
|
||||||
|
|
||||||
|
# Authenticate keystone demo
|
||||||
|
self.keystone_demo = u.authenticate_keystone_user(self.keystone,
|
||||||
|
user=self.demo_user,
|
||||||
|
password='password',
|
||||||
|
tenant=self.demo_tenant)
|
@ -1,15 +0,0 @@
|
|||||||
hacluster-mysql:
|
|
||||||
series: trusty
|
|
||||||
services:
|
|
||||||
hacluster:
|
|
||||||
charm: hacluster
|
|
||||||
num_units: 0
|
|
||||||
mysql:
|
|
||||||
charm: cs:trusty/mysql
|
|
||||||
num_units: 2
|
|
||||||
options:
|
|
||||||
"dataset-size": 128M
|
|
||||||
vip: 192.168.21.1
|
|
||||||
relations:
|
|
||||||
- - "mysql:ha"
|
|
||||||
- "hacluster:ha"
|
|
38
tests/charmhelpers/__init__.py
Normal file
38
tests/charmhelpers/__init__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
||||||
|
# only standard libraries.
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import six # flake8: noqa
|
||||||
|
except ImportError:
|
||||||
|
if sys.version_info.major == 2:
|
||||||
|
subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
|
||||||
|
else:
|
||||||
|
subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
|
||||||
|
import six # flake8: noqa
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml # flake8: noqa
|
||||||
|
except ImportError:
|
||||||
|
if sys.version_info.major == 2:
|
||||||
|
subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
|
||||||
|
else:
|
||||||
|
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
||||||
|
import yaml # flake8: noqa
|
15
tests/charmhelpers/contrib/__init__.py
Normal file
15
tests/charmhelpers/contrib/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
15
tests/charmhelpers/contrib/amulet/__init__.py
Normal file
15
tests/charmhelpers/contrib/amulet/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
93
tests/charmhelpers/contrib/amulet/deployment.py
Normal file
93
tests/charmhelpers/contrib/amulet/deployment.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import amulet
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
class AmuletDeployment(object):
|
||||||
|
"""Amulet deployment.
|
||||||
|
|
||||||
|
This class provides generic Amulet deployment and test runner
|
||||||
|
methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, series=None):
|
||||||
|
"""Initialize the deployment environment."""
|
||||||
|
self.series = None
|
||||||
|
|
||||||
|
if series:
|
||||||
|
self.series = series
|
||||||
|
self.d = amulet.Deployment(series=self.series)
|
||||||
|
else:
|
||||||
|
self.d = amulet.Deployment()
|
||||||
|
|
||||||
|
def _add_services(self, this_service, other_services):
|
||||||
|
"""Add services.
|
||||||
|
|
||||||
|
Add services to the deployment where this_service is the local charm
|
||||||
|
that we're testing and other_services are the other services that
|
||||||
|
are being used in the local amulet tests.
|
||||||
|
"""
|
||||||
|
if this_service['name'] != os.path.basename(os.getcwd()):
|
||||||
|
s = this_service['name']
|
||||||
|
msg = "The charm's root directory name needs to be {}".format(s)
|
||||||
|
amulet.raise_status(amulet.FAIL, msg=msg)
|
||||||
|
|
||||||
|
if 'units' not in this_service:
|
||||||
|
this_service['units'] = 1
|
||||||
|
|
||||||
|
self.d.add(this_service['name'], units=this_service['units'])
|
||||||
|
|
||||||
|
for svc in other_services:
|
||||||
|
if 'location' in svc:
|
||||||
|
branch_location = svc['location']
|
||||||
|
elif self.series:
|
||||||
|
branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
|
||||||
|
else:
|
||||||
|
branch_location = None
|
||||||
|
|
||||||
|
if 'units' not in svc:
|
||||||
|
svc['units'] = 1
|
||||||
|
|
||||||
|
self.d.add(svc['name'], charm=branch_location, units=svc['units'])
|
||||||
|
|
||||||
|
def _add_relations(self, relations):
|
||||||
|
"""Add all of the relations for the services."""
|
||||||
|
for k, v in six.iteritems(relations):
|
||||||
|
self.d.relate(k, v)
|
||||||
|
|
||||||
|
def _configure_services(self, configs):
|
||||||
|
"""Configure all of the services."""
|
||||||
|
for service, config in six.iteritems(configs):
|
||||||
|
self.d.configure(service, config)
|
||||||
|
|
||||||
|
def _deploy(self):
|
||||||
|
"""Deploy environment and wait for all hooks to finish executing."""
|
||||||
|
try:
|
||||||
|
self.d.setup(timeout=900)
|
||||||
|
self.d.sentry.wait(timeout=900)
|
||||||
|
except amulet.helpers.TimeoutError:
|
||||||
|
amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def run_tests(self):
|
||||||
|
"""Run all of the methods that are prefixed with 'test_'."""
|
||||||
|
for test in dir(self):
|
||||||
|
if test.startswith('test_'):
|
||||||
|
getattr(self, test)()
|
314
tests/charmhelpers/contrib/amulet/utils.py
Normal file
314
tests/charmhelpers/contrib/amulet/utils.py
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import ConfigParser
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
class AmuletUtils(object):
|
||||||
|
"""Amulet utilities.
|
||||||
|
|
||||||
|
This class provides common utility functions that are used by Amulet
|
||||||
|
tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, log_level=logging.ERROR):
|
||||||
|
self.log = self.get_logger(level=log_level)
|
||||||
|
|
||||||
|
def get_logger(self, name="amulet-logger", level=logging.DEBUG):
|
||||||
|
"""Get a logger object that will log to stdout."""
|
||||||
|
log = logging
|
||||||
|
logger = log.getLogger(name)
|
||||||
|
fmt = log.Formatter("%(asctime)s %(funcName)s "
|
||||||
|
"%(levelname)s: %(message)s")
|
||||||
|
|
||||||
|
handler = log.StreamHandler(stream=sys.stdout)
|
||||||
|
handler.setLevel(level)
|
||||||
|
handler.setFormatter(fmt)
|
||||||
|
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
def valid_ip(self, ip):
|
||||||
|
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def valid_url(self, url):
|
||||||
|
p = re.compile(
|
||||||
|
r'^(?:http|ftp)s?://'
|
||||||
|
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
|
||||||
|
r'localhost|'
|
||||||
|
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
||||||
|
r'(?::\d+)?'
|
||||||
|
r'(?:/?|[/?]\S+)$',
|
||||||
|
re.IGNORECASE)
|
||||||
|
if p.match(url):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_services(self, commands):
|
||||||
|
"""Validate services.
|
||||||
|
|
||||||
|
Verify the specified services are running on the corresponding
|
||||||
|
service units.
|
||||||
|
"""
|
||||||
|
for k, v in six.iteritems(commands):
|
||||||
|
for cmd in v:
|
||||||
|
output, code = k.run(cmd)
|
||||||
|
if code != 0:
|
||||||
|
return "command `{}` returned {}".format(cmd, str(code))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_config(self, unit, filename):
|
||||||
|
"""Get a ConfigParser object for parsing a unit's config file."""
|
||||||
|
file_contents = unit.file_contents(filename)
|
||||||
|
config = ConfigParser.ConfigParser()
|
||||||
|
config.readfp(io.StringIO(file_contents))
|
||||||
|
return config
|
||||||
|
|
||||||
|
def validate_config_data(self, sentry_unit, config_file, section,
|
||||||
|
expected):
|
||||||
|
"""Validate config file data.
|
||||||
|
|
||||||
|
Verify that the specified section of the config file contains
|
||||||
|
the expected option key:value pairs.
|
||||||
|
"""
|
||||||
|
config = self._get_config(sentry_unit, config_file)
|
||||||
|
|
||||||
|
if section != 'DEFAULT' and not config.has_section(section):
|
||||||
|
return "section [{}] does not exist".format(section)
|
||||||
|
|
||||||
|
for k in expected.keys():
|
||||||
|
if not config.has_option(section, k):
|
||||||
|
return "section [{}] is missing option {}".format(section, k)
|
||||||
|
if config.get(section, k) != expected[k]:
|
||||||
|
return "section [{}] {}:{} != expected {}:{}".format(
|
||||||
|
section, k, config.get(section, k), k, expected[k])
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _validate_dict_data(self, expected, actual):
|
||||||
|
"""Validate dictionary data.
|
||||||
|
|
||||||
|
Compare expected dictionary data vs actual dictionary data.
|
||||||
|
The values in the 'expected' dictionary can be strings, bools, ints,
|
||||||
|
longs, or can be a function that evaluate a variable and returns a
|
||||||
|
bool.
|
||||||
|
"""
|
||||||
|
for k, v in six.iteritems(expected):
|
||||||
|
if k in actual:
|
||||||
|
if (isinstance(v, six.string_types) or
|
||||||
|
isinstance(v, bool) or
|
||||||
|
isinstance(v, six.integer_types)):
|
||||||
|
if v != actual[k]:
|
||||||
|
return "{}:{}".format(k, actual[k])
|
||||||
|
elif not v(actual[k]):
|
||||||
|
return "{}:{}".format(k, actual[k])
|
||||||
|
else:
|
||||||
|
return "key '{}' does not exist".format(k)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_relation_data(self, sentry_unit, relation, expected):
|
||||||
|
"""Validate actual relation data based on expected relation data."""
|
||||||
|
actual = sentry_unit.relation(relation[0], relation[1])
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
return self._validate_dict_data(expected, actual)
|
||||||
|
|
||||||
|
def _validate_list_data(self, expected, actual):
|
||||||
|
"""Compare expected list vs actual list data."""
|
||||||
|
for e in expected:
|
||||||
|
if e not in actual:
|
||||||
|
return "expected item {} not found in actual list".format(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def not_null(self, string):
|
||||||
|
if string is not None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_file_mtime(self, sentry_unit, filename):
|
||||||
|
"""Get last modification time of file."""
|
||||||
|
return sentry_unit.file_stat(filename)['mtime']
|
||||||
|
|
||||||
|
def _get_dir_mtime(self, sentry_unit, directory):
|
||||||
|
"""Get last modification time of directory."""
|
||||||
|
return sentry_unit.directory_stat(directory)['mtime']
|
||||||
|
|
||||||
|
def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
|
||||||
|
"""Get process' start time.
|
||||||
|
|
||||||
|
Determine start time of the process based on the last modification
|
||||||
|
time of the /proc/pid directory. If pgrep_full is True, the process
|
||||||
|
name is matched against the full command line.
|
||||||
|
"""
|
||||||
|
if pgrep_full:
|
||||||
|
cmd = 'pgrep -o -f {}'.format(service)
|
||||||
|
else:
|
||||||
|
cmd = 'pgrep -o {}'.format(service)
|
||||||
|
cmd = cmd + ' | grep -v pgrep || exit 0'
|
||||||
|
cmd_out = sentry_unit.run(cmd)
|
||||||
|
self.log.debug('CMDout: ' + str(cmd_out))
|
||||||
|
if cmd_out[0]:
|
||||||
|
self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
|
||||||
|
proc_dir = '/proc/{}'.format(cmd_out[0].strip())
|
||||||
|
return self._get_dir_mtime(sentry_unit, proc_dir)
|
||||||
|
|
||||||
|
def service_restarted(self, sentry_unit, service, filename,
|
||||||
|
pgrep_full=False, sleep_time=20):
|
||||||
|
"""Check if service was restarted.
|
||||||
|
|
||||||
|
Compare a service's start time vs a file's last modification time
|
||||||
|
(such as a config file for that service) to determine if the service
|
||||||
|
has been restarted.
|
||||||
|
"""
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
|
||||||
|
self._get_file_mtime(sentry_unit, filename)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def service_restarted_since(self, sentry_unit, mtime, service,
|
||||||
|
pgrep_full=False, sleep_time=20,
|
||||||
|
retry_count=2):
|
||||||
|
"""Check if service was been started after a given time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sentry_unit (sentry): The sentry unit to check for the service on
|
||||||
|
mtime (float): The epoch time to check against
|
||||||
|
service (string): service name to look for in process table
|
||||||
|
pgrep_full (boolean): Use full command line search mode with pgrep
|
||||||
|
sleep_time (int): Seconds to sleep before looking for process
|
||||||
|
retry_count (int): If service is not found, how many times to retry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if service found and its start time it newer than mtime,
|
||||||
|
False if service is older than mtime or if service was
|
||||||
|
not found.
|
||||||
|
"""
|
||||||
|
self.log.debug('Checking %s restarted since %s' % (service, mtime))
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
proc_start_time = self._get_proc_start_time(sentry_unit, service,
|
||||||
|
pgrep_full)
|
||||||
|
while retry_count > 0 and not proc_start_time:
|
||||||
|
self.log.debug('No pid file found for service %s, will retry %i '
|
||||||
|
'more times' % (service, retry_count))
|
||||||
|
time.sleep(30)
|
||||||
|
proc_start_time = self._get_proc_start_time(sentry_unit, service,
|
||||||
|
pgrep_full)
|
||||||
|
retry_count = retry_count - 1
|
||||||
|
|
||||||
|
if not proc_start_time:
|
||||||
|
self.log.warn('No proc start time found, assuming service did '
|
||||||
|
'not start')
|
||||||
|
return False
|
||||||
|
if proc_start_time >= mtime:
|
||||||
|
self.log.debug('proc start time is newer than provided mtime'
|
||||||
|
'(%s >= %s)' % (proc_start_time, mtime))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.log.warn('proc start time (%s) is older than provided mtime '
|
||||||
|
'(%s), service did not restart' % (proc_start_time,
|
||||||
|
mtime))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def config_updated_since(self, sentry_unit, filename, mtime,
|
||||||
|
sleep_time=20):
|
||||||
|
"""Check if file was modified after a given time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sentry_unit (sentry): The sentry unit to check the file mtime on
|
||||||
|
filename (string): The file to check mtime of
|
||||||
|
mtime (float): The epoch time to check against
|
||||||
|
sleep_time (int): Seconds to sleep before looking for process
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if file was modified more recently than mtime, False if
|
||||||
|
file was modified before mtime,
|
||||||
|
"""
|
||||||
|
self.log.debug('Checking %s updated since %s' % (filename, mtime))
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
file_mtime = self._get_file_mtime(sentry_unit, filename)
|
||||||
|
if file_mtime >= mtime:
|
||||||
|
self.log.debug('File mtime is newer than provided mtime '
|
||||||
|
'(%s >= %s)' % (file_mtime, mtime))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.log.warn('File mtime %s is older than provided mtime %s'
|
||||||
|
% (file_mtime, mtime))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_service_config_changed(self, sentry_unit, mtime, service,
|
||||||
|
filename, pgrep_full=False,
|
||||||
|
sleep_time=20, retry_count=2):
|
||||||
|
"""Check service and file were updated after mtime
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sentry_unit (sentry): The sentry unit to check for the service on
|
||||||
|
mtime (float): The epoch time to check against
|
||||||
|
service (string): service name to look for in process table
|
||||||
|
filename (string): The file to check mtime of
|
||||||
|
pgrep_full (boolean): Use full command line search mode with pgrep
|
||||||
|
sleep_time (int): Seconds to sleep before looking for process
|
||||||
|
retry_count (int): If service is not found, how many times to retry
|
||||||
|
|
||||||
|
Typical Usage:
|
||||||
|
u = OpenStackAmuletUtils(ERROR)
|
||||||
|
...
|
||||||
|
mtime = u.get_sentry_time(self.cinder_sentry)
|
||||||
|
self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
|
||||||
|
if not u.validate_service_config_changed(self.cinder_sentry,
|
||||||
|
mtime,
|
||||||
|
'cinder-api',
|
||||||
|
'/etc/cinder/cinder.conf')
|
||||||
|
amulet.raise_status(amulet.FAIL, msg='update failed')
|
||||||
|
Returns:
|
||||||
|
bool: True if both service and file where updated/restarted after
|
||||||
|
mtime, False if service is older than mtime or if service was
|
||||||
|
not found or if filename was modified before mtime.
|
||||||
|
"""
|
||||||
|
self.log.debug('Checking %s restarted since %s' % (service, mtime))
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
service_restart = self.service_restarted_since(sentry_unit, mtime,
|
||||||
|
service,
|
||||||
|
pgrep_full=pgrep_full,
|
||||||
|
sleep_time=0,
|
||||||
|
retry_count=retry_count)
|
||||||
|
config_update = self.config_updated_since(sentry_unit, filename, mtime,
|
||||||
|
sleep_time=0)
|
||||||
|
return service_restart and config_update
|
||||||
|
|
||||||
|
def get_sentry_time(self, sentry_unit):
|
||||||
|
"""Return current epoch time on a sentry"""
|
||||||
|
cmd = "date +'%s'"
|
||||||
|
return float(sentry_unit.run(cmd)[0])
|
||||||
|
|
||||||
|
def relation_error(self, name, data):
|
||||||
|
return 'unexpected relation data in {} - {}'.format(name, data)
|
||||||
|
|
||||||
|
def endpoint_error(self, name, data):
|
||||||
|
return 'unexpected endpoint data in {} - {}'.format(name, data)
|
15
tests/charmhelpers/contrib/openstack/__init__.py
Normal file
15
tests/charmhelpers/contrib/openstack/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
15
tests/charmhelpers/contrib/openstack/amulet/__init__.py
Normal file
15
tests/charmhelpers/contrib/openstack/amulet/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
111
tests/charmhelpers/contrib/openstack/amulet/deployment.py
Normal file
111
tests/charmhelpers/contrib/openstack/amulet/deployment.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import six
|
||||||
|
from charmhelpers.contrib.amulet.deployment import (
|
||||||
|
AmuletDeployment
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackAmuletDeployment(AmuletDeployment):
|
||||||
|
"""OpenStack amulet deployment.
|
||||||
|
|
||||||
|
This class inherits from AmuletDeployment and has additional support
|
||||||
|
that is specifically for use by OpenStack charms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, series=None, openstack=None, source=None, stable=True):
|
||||||
|
"""Initialize the deployment environment."""
|
||||||
|
super(OpenStackAmuletDeployment, self).__init__(series)
|
||||||
|
self.openstack = openstack
|
||||||
|
self.source = source
|
||||||
|
self.stable = stable
|
||||||
|
# Note(coreycb): this needs to be changed when new next branches come
|
||||||
|
# out.
|
||||||
|
self.current_next = "trusty"
|
||||||
|
|
||||||
|
def _determine_branch_locations(self, other_services):
|
||||||
|
"""Determine the branch locations for the other services.
|
||||||
|
|
||||||
|
Determine if the local branch being tested is derived from its
|
||||||
|
stable or next (dev) branch, and based on this, use the corresonding
|
||||||
|
stable or next branches for the other_services."""
|
||||||
|
base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
|
||||||
|
|
||||||
|
if self.stable:
|
||||||
|
for svc in other_services:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
for svc in other_services:
|
||||||
|
if svc['name'] in base_charms:
|
||||||
|
temp = 'lp:charms/{}'
|
||||||
|
svc['location'] = temp.format(svc['name'])
|
||||||
|
else:
|
||||||
|
temp = 'lp:~openstack-charmers/charms/{}/{}/next'
|
||||||
|
svc['location'] = temp.format(self.current_next,
|
||||||
|
svc['name'])
|
||||||
|
return other_services
|
||||||
|
|
||||||
|
def _add_services(self, this_service, other_services):
|
||||||
|
"""Add services to the deployment and set openstack-origin/source."""
|
||||||
|
other_services = self._determine_branch_locations(other_services)
|
||||||
|
|
||||||
|
super(OpenStackAmuletDeployment, self)._add_services(this_service,
|
||||||
|
other_services)
|
||||||
|
|
||||||
|
services = other_services
|
||||||
|
services.append(this_service)
|
||||||
|
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
|
||||||
|
'ceph-osd', 'ceph-radosgw']
|
||||||
|
# Openstack subordinate charms do not expose an origin option as that
|
||||||
|
# is controlled by the principle
|
||||||
|
ignore = ['neutron-openvswitch']
|
||||||
|
|
||||||
|
if self.openstack:
|
||||||
|
for svc in services:
|
||||||
|
if svc['name'] not in use_source + ignore:
|
||||||
|
config = {'openstack-origin': self.openstack}
|
||||||
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
|
if self.source:
|
||||||
|
for svc in services:
|
||||||
|
if svc['name'] in use_source and svc['name'] not in ignore:
|
||||||
|
config = {'source': self.source}
|
||||||
|
self.d.configure(svc['name'], config)
|
||||||
|
|
||||||
|
def _configure_services(self, configs):
|
||||||
|
"""Configure all of the services."""
|
||||||
|
for service, config in six.iteritems(configs):
|
||||||
|
self.d.configure(service, config)
|
||||||
|
|
||||||
|
def _get_openstack_release(self):
|
||||||
|
"""Get openstack release.
|
||||||
|
|
||||||
|
Return an integer representing the enum value of the openstack
|
||||||
|
release.
|
||||||
|
"""
|
||||||
|
(self.precise_essex, self.precise_folsom, self.precise_grizzly,
|
||||||
|
self.precise_havana, self.precise_icehouse,
|
||||||
|
self.trusty_icehouse) = range(6)
|
||||||
|
releases = {
|
||||||
|
('precise', None): self.precise_essex,
|
||||||
|
('precise', 'cloud:precise-folsom'): self.precise_folsom,
|
||||||
|
('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
|
||||||
|
('precise', 'cloud:precise-havana'): self.precise_havana,
|
||||||
|
('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
|
||||||
|
('trusty', None): self.trusty_icehouse}
|
||||||
|
return releases[(self.series, self.openstack)]
|
294
tests/charmhelpers/contrib/openstack/amulet/utils.py
Normal file
294
tests/charmhelpers/contrib/openstack/amulet/utils.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import glanceclient.v1.client as glance_client
|
||||||
|
import keystoneclient.v2_0 as keystone_client
|
||||||
|
import novaclient.v1_1.client as nova_client
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from charmhelpers.contrib.amulet.utils import (
|
||||||
|
AmuletUtils
|
||||||
|
)
|
||||||
|
|
||||||
|
DEBUG = logging.DEBUG
|
||||||
|
ERROR = logging.ERROR
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackAmuletUtils(AmuletUtils):
|
||||||
|
"""OpenStack amulet utilities.
|
||||||
|
|
||||||
|
This class inherits from AmuletUtils and has additional support
|
||||||
|
that is specifically for use by OpenStack charms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, log_level=ERROR):
|
||||||
|
"""Initialize the deployment environment."""
|
||||||
|
super(OpenStackAmuletUtils, self).__init__(log_level)
|
||||||
|
|
||||||
|
def validate_endpoint_data(self, endpoints, admin_port, internal_port,
|
||||||
|
public_port, expected):
|
||||||
|
"""Validate endpoint data.
|
||||||
|
|
||||||
|
Validate actual endpoint data vs expected endpoint data. The ports
|
||||||
|
are used to find the matching endpoint.
|
||||||
|
"""
|
||||||
|
found = False
|
||||||
|
for ep in endpoints:
|
||||||
|
self.log.debug('endpoint: {}'.format(repr(ep)))
|
||||||
|
if (admin_port in ep.adminurl and
|
||||||
|
internal_port in ep.internalurl and
|
||||||
|
public_port in ep.publicurl):
|
||||||
|
found = True
|
||||||
|
actual = {'id': ep.id,
|
||||||
|
'region': ep.region,
|
||||||
|
'adminurl': ep.adminurl,
|
||||||
|
'internalurl': ep.internalurl,
|
||||||
|
'publicurl': ep.publicurl,
|
||||||
|
'service_id': ep.service_id}
|
||||||
|
ret = self._validate_dict_data(expected, actual)
|
||||||
|
if ret:
|
||||||
|
return 'unexpected endpoint data - {}'.format(ret)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
return 'endpoint not found'
|
||||||
|
|
||||||
|
def validate_svc_catalog_endpoint_data(self, expected, actual):
|
||||||
|
"""Validate service catalog endpoint data.
|
||||||
|
|
||||||
|
Validate a list of actual service catalog endpoints vs a list of
|
||||||
|
expected service catalog endpoints.
|
||||||
|
"""
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
for k, v in six.iteritems(expected):
|
||||||
|
if k in actual:
|
||||||
|
ret = self._validate_dict_data(expected[k][0], actual[k][0])
|
||||||
|
if ret:
|
||||||
|
return self.endpoint_error(k, ret)
|
||||||
|
else:
|
||||||
|
return "endpoint {} does not exist".format(k)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def validate_tenant_data(self, expected, actual):
|
||||||
|
"""Validate tenant data.
|
||||||
|
|
||||||
|
Validate a list of actual tenant data vs list of expected tenant
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
for e in expected:
|
||||||
|
found = False
|
||||||
|
for act in actual:
|
||||||
|
a = {'enabled': act.enabled, 'description': act.description,
|
||||||
|
'name': act.name, 'id': act.id}
|
||||||
|
if e['name'] == a['name']:
|
||||||
|
found = True
|
||||||
|
ret = self._validate_dict_data(e, a)
|
||||||
|
if ret:
|
||||||
|
return "unexpected tenant data - {}".format(ret)
|
||||||
|
if not found:
|
||||||
|
return "tenant {} does not exist".format(e['name'])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def validate_role_data(self, expected, actual):
|
||||||
|
"""Validate role data.
|
||||||
|
|
||||||
|
Validate a list of actual role data vs a list of expected role
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
for e in expected:
|
||||||
|
found = False
|
||||||
|
for act in actual:
|
||||||
|
a = {'name': act.name, 'id': act.id}
|
||||||
|
if e['name'] == a['name']:
|
||||||
|
found = True
|
||||||
|
ret = self._validate_dict_data(e, a)
|
||||||
|
if ret:
|
||||||
|
return "unexpected role data - {}".format(ret)
|
||||||
|
if not found:
|
||||||
|
return "role {} does not exist".format(e['name'])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def validate_user_data(self, expected, actual):
|
||||||
|
"""Validate user data.
|
||||||
|
|
||||||
|
Validate a list of actual user data vs a list of expected user
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
for e in expected:
|
||||||
|
found = False
|
||||||
|
for act in actual:
|
||||||
|
a = {'enabled': act.enabled, 'name': act.name,
|
||||||
|
'email': act.email, 'tenantId': act.tenantId,
|
||||||
|
'id': act.id}
|
||||||
|
if e['name'] == a['name']:
|
||||||
|
found = True
|
||||||
|
ret = self._validate_dict_data(e, a)
|
||||||
|
if ret:
|
||||||
|
return "unexpected user data - {}".format(ret)
|
||||||
|
if not found:
|
||||||
|
return "user {} does not exist".format(e['name'])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def validate_flavor_data(self, expected, actual):
|
||||||
|
"""Validate flavor data.
|
||||||
|
|
||||||
|
Validate a list of actual flavors vs a list of expected flavors.
|
||||||
|
"""
|
||||||
|
self.log.debug('actual: {}'.format(repr(actual)))
|
||||||
|
act = [a.name for a in actual]
|
||||||
|
return self._validate_list_data(expected, act)
|
||||||
|
|
||||||
|
def tenant_exists(self, keystone, tenant):
|
||||||
|
"""Return True if tenant exists."""
|
||||||
|
return tenant in [t.name for t in keystone.tenants.list()]
|
||||||
|
|
||||||
|
def authenticate_keystone_admin(self, keystone_sentry, user, password,
|
||||||
|
tenant):
|
||||||
|
"""Authenticates admin user with the keystone admin endpoint."""
|
||||||
|
unit = keystone_sentry
|
||||||
|
service_ip = unit.relation('shared-db',
|
||||||
|
'mysql:shared-db')['private-address']
|
||||||
|
ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
|
||||||
|
return keystone_client.Client(username=user, password=password,
|
||||||
|
tenant_name=tenant, auth_url=ep)
|
||||||
|
|
||||||
|
def authenticate_keystone_user(self, keystone, user, password, tenant):
|
||||||
|
"""Authenticates a regular user with the keystone public endpoint."""
|
||||||
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
|
endpoint_type='publicURL')
|
||||||
|
return keystone_client.Client(username=user, password=password,
|
||||||
|
tenant_name=tenant, auth_url=ep)
|
||||||
|
|
||||||
|
def authenticate_glance_admin(self, keystone):
|
||||||
|
"""Authenticates admin user with glance."""
|
||||||
|
ep = keystone.service_catalog.url_for(service_type='image',
|
||||||
|
endpoint_type='adminURL')
|
||||||
|
return glance_client.Client(ep, token=keystone.auth_token)
|
||||||
|
|
||||||
|
def authenticate_nova_user(self, keystone, user, password, tenant):
|
||||||
|
"""Authenticates a regular user with nova-api."""
|
||||||
|
ep = keystone.service_catalog.url_for(service_type='identity',
|
||||||
|
endpoint_type='publicURL')
|
||||||
|
return nova_client.Client(username=user, api_key=password,
|
||||||
|
project_id=tenant, auth_url=ep)
|
||||||
|
|
||||||
|
def create_cirros_image(self, glance, image_name):
|
||||||
|
"""Download the latest cirros image and upload it to glance."""
|
||||||
|
http_proxy = os.getenv('AMULET_HTTP_PROXY')
|
||||||
|
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
|
||||||
|
if http_proxy:
|
||||||
|
proxies = {'http': http_proxy}
|
||||||
|
opener = urllib.FancyURLopener(proxies)
|
||||||
|
else:
|
||||||
|
opener = urllib.FancyURLopener()
|
||||||
|
|
||||||
|
f = opener.open("http://download.cirros-cloud.net/version/released")
|
||||||
|
version = f.read().strip()
|
||||||
|
cirros_img = "cirros-{}-x86_64-disk.img".format(version)
|
||||||
|
local_path = os.path.join('tests', cirros_img)
|
||||||
|
|
||||||
|
if not os.path.exists(local_path):
|
||||||
|
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
|
||||||
|
version, cirros_img)
|
||||||
|
opener.retrieve(cirros_url, local_path)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
with open(local_path) as f:
|
||||||
|
image = glance.images.create(name=image_name, is_public=True,
|
||||||
|
disk_format='qcow2',
|
||||||
|
container_format='bare', data=f)
|
||||||
|
count = 1
|
||||||
|
status = image.status
|
||||||
|
while status != 'active' and count < 10:
|
||||||
|
time.sleep(3)
|
||||||
|
image = glance.images.get(image.id)
|
||||||
|
status = image.status
|
||||||
|
self.log.debug('image status: {}'.format(status))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if status != 'active':
|
||||||
|
self.log.error('image creation timed out')
|
||||||
|
return None
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
def delete_image(self, glance, image):
|
||||||
|
"""Delete the specified image."""
|
||||||
|
num_before = len(list(glance.images.list()))
|
||||||
|
glance.images.delete(image)
|
||||||
|
|
||||||
|
count = 1
|
||||||
|
num_after = len(list(glance.images.list()))
|
||||||
|
while num_after != (num_before - 1) and count < 10:
|
||||||
|
time.sleep(3)
|
||||||
|
num_after = len(list(glance.images.list()))
|
||||||
|
self.log.debug('number of images: {}'.format(num_after))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if num_after != (num_before - 1):
|
||||||
|
self.log.error('image deletion timed out')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_instance(self, nova, image_name, instance_name, flavor):
|
||||||
|
"""Create the specified instance."""
|
||||||
|
image = nova.images.find(name=image_name)
|
||||||
|
flavor = nova.flavors.find(name=flavor)
|
||||||
|
instance = nova.servers.create(name=instance_name, image=image,
|
||||||
|
flavor=flavor)
|
||||||
|
|
||||||
|
count = 1
|
||||||
|
status = instance.status
|
||||||
|
while status != 'ACTIVE' and count < 60:
|
||||||
|
time.sleep(3)
|
||||||
|
instance = nova.servers.get(instance.id)
|
||||||
|
status = instance.status
|
||||||
|
self.log.debug('instance status: {}'.format(status))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if status != 'ACTIVE':
|
||||||
|
self.log.error('instance creation timed out')
|
||||||
|
return None
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def delete_instance(self, nova, instance):
|
||||||
|
"""Delete the specified instance."""
|
||||||
|
num_before = len(list(nova.servers.list()))
|
||||||
|
nova.servers.delete(instance)
|
||||||
|
|
||||||
|
count = 1
|
||||||
|
num_after = len(list(nova.servers.list()))
|
||||||
|
while num_after != (num_before - 1) and count < 10:
|
||||||
|
time.sleep(3)
|
||||||
|
num_after = len(list(nova.servers.list()))
|
||||||
|
self.log.debug('number of instances: {}'.format(num_after))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if num_after != (num_before - 1):
|
||||||
|
self.log.error('instance deletion timed out')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
@ -1,123 +1,42 @@
|
|||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
with mock.patch('charmhelpers.core.hookenv.config'):
|
import hooks
|
||||||
import hooks as hacluster_hooks
|
|
||||||
|
|
||||||
|
|
||||||
def local_log(msg, level='INFO'):
|
@mock.patch.object(hooks, 'log', lambda *args, **kwargs: None)
|
||||||
print('[{}] {}'.format(level, msg))
|
@mock.patch('utils.COROSYNC_CONF', os.path.join(tempfile.mkdtemp(),
|
||||||
|
'corosync.conf'))
|
||||||
|
|
||||||
def write_file(path, content, *args, **kwargs):
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
|
|
||||||
class SwiftContextTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
@mock.patch('hooks.config')
|
|
||||||
def test_get_transport(self, mock_config):
|
|
||||||
mock_config.return_value = 'udp'
|
|
||||||
self.assertEqual('udp', hacluster_hooks.get_transport())
|
|
||||||
|
|
||||||
mock_config.return_value = 'udpu'
|
|
||||||
self.assertEqual('udpu', hacluster_hooks.get_transport())
|
|
||||||
|
|
||||||
mock_config.return_value = 'hafu'
|
|
||||||
self.assertRaises(ValueError, hacluster_hooks.get_transport)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('hooks.log', local_log)
|
|
||||||
@mock.patch('hooks.write_file', write_file)
|
|
||||||
class TestCorosyncConf(unittest.TestCase):
|
class TestCorosyncConf(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmpdir = tempfile.mkdtemp()
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
hacluster_hooks.COROSYNC_CONF = os.path.join(self.tmpdir,
|
|
||||||
'corosync.conf')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(self.tmpdir)
|
|
||||||
|
|
||||||
def test_debug_on(self):
|
|
||||||
self.check_debug(True)
|
|
||||||
|
|
||||||
def test_debug_off(self):
|
|
||||||
self.check_debug(False)
|
|
||||||
|
|
||||||
@mock.patch('hooks.relation_get')
|
|
||||||
@mock.patch('hooks.related_units')
|
|
||||||
@mock.patch('hooks.relation_ids')
|
|
||||||
@mock.patch('hacluster.get_network_address')
|
|
||||||
@mock.patch('hooks.config')
|
|
||||||
def check_debug(self, enabled, mock_config, get_network_address,
|
|
||||||
relation_ids, related_units, relation_get):
|
|
||||||
cfg = {'debug': enabled,
|
|
||||||
'prefer-ipv6': False,
|
|
||||||
'corosync_transport': 'udpu',
|
|
||||||
'corosync_mcastaddr': 'corosync_mcastaddr'}
|
|
||||||
|
|
||||||
def c(k):
|
|
||||||
return cfg.get(k)
|
|
||||||
|
|
||||||
mock_config.side_effect = c
|
|
||||||
get_network_address.return_value = "127.0.0.1"
|
|
||||||
relation_ids.return_value = ['foo:1']
|
|
||||||
related_units.return_value = ['unit-machine-0']
|
|
||||||
relation_get.return_value = 'iface'
|
|
||||||
|
|
||||||
hacluster_hooks.get_ha_nodes = mock.MagicMock()
|
|
||||||
conf = hacluster_hooks.get_corosync_conf()
|
|
||||||
self.assertEqual(conf['debug'], enabled)
|
|
||||||
|
|
||||||
self.assertTrue(hacluster_hooks.emit_corosync_conf())
|
|
||||||
|
|
||||||
with open(hacluster_hooks.COROSYNC_CONF) as fd:
|
|
||||||
content = fd.read()
|
|
||||||
if enabled:
|
|
||||||
pattern = 'debug: on\n'
|
|
||||||
else:
|
|
||||||
pattern = 'debug: off\n'
|
|
||||||
|
|
||||||
matches = re.findall(pattern, content, re.M)
|
|
||||||
self.assertEqual(len(matches), 2, str(matches))
|
|
||||||
|
|
||||||
@mock.patch('pcmk.wait_for_pcmk')
|
@mock.patch('pcmk.wait_for_pcmk')
|
||||||
@mock.patch('hooks.peer_units')
|
@mock.patch.object(hooks, 'peer_units')
|
||||||
@mock.patch('pcmk.crm_opt_exists')
|
@mock.patch('pcmk.crm_opt_exists')
|
||||||
@mock.patch('hooks.oldest_peer')
|
@mock.patch.object(hooks, 'oldest_peer')
|
||||||
@mock.patch('hooks.configure_corosync')
|
@mock.patch.object(hooks, 'configure_corosync')
|
||||||
@mock.patch('hooks.configure_cluster_global')
|
@mock.patch.object(hooks, 'configure_cluster_global')
|
||||||
@mock.patch('hooks.configure_monitor_host')
|
@mock.patch.object(hooks, 'configure_monitor_host')
|
||||||
@mock.patch('hooks.configure_stonith')
|
@mock.patch.object(hooks, 'configure_stonith')
|
||||||
@mock.patch('hooks.related_units')
|
@mock.patch.object(hooks, 'related_units')
|
||||||
@mock.patch('hooks.get_cluster_nodes')
|
@mock.patch.object(hooks, 'get_cluster_nodes')
|
||||||
@mock.patch('hooks.relation_set')
|
@mock.patch.object(hooks, 'relation_set')
|
||||||
@mock.patch('hooks.relation_ids')
|
@mock.patch.object(hooks, 'relation_ids')
|
||||||
@mock.patch('hooks.get_corosync_conf')
|
@mock.patch.object(hooks, 'get_corosync_conf')
|
||||||
@mock.patch('pcmk.commit')
|
@mock.patch('pcmk.commit')
|
||||||
@mock.patch('hooks.config')
|
@mock.patch.object(hooks, 'config')
|
||||||
@mock.patch('hooks.parse_data')
|
@mock.patch.object(hooks, 'parse_data')
|
||||||
def test_configure_principle_cluster_resources(self, parse_data, config,
|
def test_ha_relation_changed(self, parse_data, config, commit,
|
||||||
commit,
|
get_corosync_conf, relation_ids, relation_set,
|
||||||
get_corosync_conf,
|
get_cluster_nodes, related_units,
|
||||||
relation_ids, relation_set,
|
configure_stonith, configure_monitor_host,
|
||||||
get_cluster_nodes,
|
configure_cluster_global, configure_corosync,
|
||||||
related_units,
|
oldest_peer, crm_opt_exists, peer_units,
|
||||||
configure_stonith,
|
wait_for_pcmk):
|
||||||
configure_monitor_host,
|
|
||||||
configure_cluster_global,
|
|
||||||
configure_corosync,
|
|
||||||
oldest_peer, crm_opt_exists,
|
|
||||||
peer_units, wait_for_pcmk):
|
|
||||||
crm_opt_exists.return_value = False
|
crm_opt_exists.return_value = False
|
||||||
oldest_peer.return_value = True
|
oldest_peer.return_value = True
|
||||||
related_units.return_value = ['ha/0', 'ha/1', 'ha/2']
|
related_units.return_value = ['ha/0', 'ha/1', 'ha/2']
|
||||||
@ -130,10 +49,7 @@ class TestCorosyncConf(unittest.TestCase):
|
|||||||
'corosync_mcastaddr': 'corosync_mcastaddr',
|
'corosync_mcastaddr': 'corosync_mcastaddr',
|
||||||
'cluster_count': 3}
|
'cluster_count': 3}
|
||||||
|
|
||||||
def c(k):
|
config.side_effect = lambda key: cfg.get(key)
|
||||||
return cfg.get(k)
|
|
||||||
|
|
||||||
config.side_effect = c
|
|
||||||
|
|
||||||
rel_get_data = {'locations': {'loc_foo': 'bar rule inf: meh eq 1'},
|
rel_get_data = {'locations': {'loc_foo': 'bar rule inf: meh eq 1'},
|
||||||
'clones': {'cl_foo': 'res_foo meta interleave=true'},
|
'clones': {'cl_foo': 'res_foo meta interleave=true'},
|
||||||
@ -150,7 +66,7 @@ class TestCorosyncConf(unittest.TestCase):
|
|||||||
|
|
||||||
parse_data.side_effect = fake_parse_data
|
parse_data.side_effect = fake_parse_data
|
||||||
|
|
||||||
hacluster_hooks.configure_principle_cluster_resources()
|
hooks.ha_relation_changed()
|
||||||
relation_set.assert_any_call(relation_id='hanode:1', ready=True)
|
relation_set.assert_any_call(relation_id='hanode:1', ready=True)
|
||||||
configure_stonith.assert_called_with()
|
configure_stonith.assert_called_with()
|
||||||
configure_monitor_host.assert_called_with()
|
configure_monitor_host.assert_called_with()
|
||||||
|
80
unit_tests/test_hacluster_utils.py
Normal file
80
unit_tests/test_hacluster_utils.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(path, content, *args, **kwargs):
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
f.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'log', lambda *args, **kwargs: None)
|
||||||
|
@mock.patch.object(utils, 'write_file', write_file)
|
||||||
|
class UtilsTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
utils.COROSYNC_CONF = os.path.join(self.tmpdir, 'corosync.conf')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tmpdir)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'relation_get')
|
||||||
|
@mock.patch.object(utils, 'related_units')
|
||||||
|
@mock.patch.object(utils, 'relation_ids')
|
||||||
|
@mock.patch.object(utils, 'get_network_address')
|
||||||
|
@mock.patch.object(utils, 'config')
|
||||||
|
def check_debug(self, enabled, mock_config, get_network_address,
|
||||||
|
relation_ids, related_units, relation_get):
|
||||||
|
cfg = {'debug': enabled,
|
||||||
|
'prefer-ipv6': False,
|
||||||
|
'corosync_transport': 'udpu',
|
||||||
|
'corosync_mcastaddr': 'corosync_mcastaddr'}
|
||||||
|
|
||||||
|
def c(k):
|
||||||
|
return cfg.get(k)
|
||||||
|
|
||||||
|
mock_config.side_effect = c
|
||||||
|
get_network_address.return_value = "127.0.0.1"
|
||||||
|
relation_ids.return_value = ['foo:1']
|
||||||
|
related_units.return_value = ['unit-machine-0']
|
||||||
|
relation_get.return_value = 'iface'
|
||||||
|
|
||||||
|
utils.get_ha_nodes = mock.MagicMock()
|
||||||
|
conf = utils.get_corosync_conf()
|
||||||
|
self.assertEqual(conf['debug'], enabled)
|
||||||
|
|
||||||
|
self.assertTrue(utils.emit_corosync_conf())
|
||||||
|
|
||||||
|
with open(utils.COROSYNC_CONF) as fd:
|
||||||
|
content = fd.read()
|
||||||
|
if enabled:
|
||||||
|
pattern = 'debug: on\n'
|
||||||
|
else:
|
||||||
|
pattern = 'debug: off\n'
|
||||||
|
|
||||||
|
matches = re.findall(pattern, content, re.M)
|
||||||
|
self.assertEqual(len(matches), 2, str(matches))
|
||||||
|
|
||||||
|
def test_debug_on(self):
|
||||||
|
self.check_debug(True)
|
||||||
|
|
||||||
|
def test_debug_off(self):
|
||||||
|
self.check_debug(False)
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'config')
|
||||||
|
def test_get_transport(self, mock_config):
|
||||||
|
mock_config.return_value = 'udp'
|
||||||
|
self.assertEqual('udp', utils.get_transport())
|
||||||
|
|
||||||
|
mock_config.return_value = 'udpu'
|
||||||
|
self.assertEqual('udpu', utils.get_transport())
|
||||||
|
|
||||||
|
mock_config.return_value = 'hafu'
|
||||||
|
self.assertRaises(ValueError, utils.get_transport)
|
Loading…
Reference in New Issue
Block a user