[tinwood, r=thedac] Add service and port checks to assess_status()
This commit is contained in:
@@ -456,3 +456,18 @@ def get_hostname(address, fqdn=True):
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
return result.split('.')[0]
|
return result.split('.')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def port_has_listener(address, port):
|
||||||
|
"""
|
||||||
|
Returns True if the address:port is open and being listened to,
|
||||||
|
else False.
|
||||||
|
|
||||||
|
@param address: an IP address or hostname
|
||||||
|
@param port: integer port
|
||||||
|
|
||||||
|
Note calls 'zc' via a subprocess shell
|
||||||
|
"""
|
||||||
|
cmd = ['nc', '-z', address, str(port)]
|
||||||
|
result = subprocess.call(cmd)
|
||||||
|
return not(bool(result))
|
||||||
|
@@ -410,6 +410,7 @@ class IdentityServiceContext(OSContextGenerator):
|
|||||||
auth_host = format_ipv6_addr(auth_host) or auth_host
|
auth_host = format_ipv6_addr(auth_host) or auth_host
|
||||||
svc_protocol = rdata.get('service_protocol') or 'http'
|
svc_protocol = rdata.get('service_protocol') or 'http'
|
||||||
auth_protocol = rdata.get('auth_protocol') or 'http'
|
auth_protocol = rdata.get('auth_protocol') or 'http'
|
||||||
|
api_version = rdata.get('api_version') or '2.0'
|
||||||
ctxt.update({'service_port': rdata.get('service_port'),
|
ctxt.update({'service_port': rdata.get('service_port'),
|
||||||
'service_host': serv_host,
|
'service_host': serv_host,
|
||||||
'auth_host': auth_host,
|
'auth_host': auth_host,
|
||||||
@@ -418,7 +419,8 @@ class IdentityServiceContext(OSContextGenerator):
|
|||||||
'admin_user': rdata.get('service_username'),
|
'admin_user': rdata.get('service_username'),
|
||||||
'admin_password': rdata.get('service_password'),
|
'admin_password': rdata.get('service_password'),
|
||||||
'service_protocol': svc_protocol,
|
'service_protocol': svc_protocol,
|
||||||
'auth_protocol': auth_protocol})
|
'auth_protocol': auth_protocol,
|
||||||
|
'api_version': api_version})
|
||||||
|
|
||||||
if self.context_complete(ctxt):
|
if self.context_complete(ctxt):
|
||||||
# NOTE(jamespage) this is required for >= icehouse
|
# NOTE(jamespage) this is required for >= icehouse
|
||||||
@@ -1471,6 +1473,8 @@ class NetworkServiceContext(OSContextGenerator):
|
|||||||
rdata.get('service_protocol') or 'http',
|
rdata.get('service_protocol') or 'http',
|
||||||
'auth_protocol':
|
'auth_protocol':
|
||||||
rdata.get('auth_protocol') or 'http',
|
rdata.get('auth_protocol') or 'http',
|
||||||
|
'api_version':
|
||||||
|
rdata.get('api_version') or '2.0',
|
||||||
}
|
}
|
||||||
if self.context_complete(ctxt):
|
if self.context_complete(ctxt):
|
||||||
return ctxt
|
return ctxt
|
||||||
|
@@ -1,4 +1,14 @@
|
|||||||
{% if auth_host -%}
|
{% if auth_host -%}
|
||||||
|
{% if api_version == '3' -%}
|
||||||
|
[keystone_authtoken]
|
||||||
|
auth_url = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
|
||||||
|
project_name = {{ admin_tenant_name }}
|
||||||
|
username = {{ admin_user }}
|
||||||
|
password = {{ admin_password }}
|
||||||
|
project_domain_name = default
|
||||||
|
user_domain_name = default
|
||||||
|
auth_plugin = password
|
||||||
|
{% else -%}
|
||||||
[keystone_authtoken]
|
[keystone_authtoken]
|
||||||
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
|
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
|
||||||
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
|
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
|
||||||
@@ -7,3 +17,4 @@ admin_user = {{ admin_user }}
|
|||||||
admin_password = {{ admin_password }}
|
admin_password = {{ admin_password }}
|
||||||
signing_dir = {{ signing_dir }}
|
signing_dir = {{ signing_dir }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
{% endif -%}
|
||||||
|
@@ -23,6 +23,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import itertools
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -60,6 +61,7 @@ from charmhelpers.contrib.storage.linux.lvm import (
|
|||||||
from charmhelpers.contrib.network.ip import (
|
from charmhelpers.contrib.network.ip import (
|
||||||
get_ipv6_addr,
|
get_ipv6_addr,
|
||||||
is_ipv6,
|
is_ipv6,
|
||||||
|
port_has_listener,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.python.packages import (
|
from charmhelpers.contrib.python.packages import (
|
||||||
@@ -67,7 +69,7 @@ from charmhelpers.contrib.python.packages import (
|
|||||||
pip_install,
|
pip_install,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.host import lsb_release, mounts, umount
|
from charmhelpers.core.host import lsb_release, mounts, umount, service_running
|
||||||
from charmhelpers.fetch import apt_install, apt_cache, install_remote
|
from charmhelpers.fetch import apt_install, apt_cache, install_remote
|
||||||
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
||||||
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
||||||
@@ -860,13 +862,23 @@ def os_workload_status(configs, required_interfaces, charm_func=None):
|
|||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
def set_os_workload_status(configs, required_interfaces, charm_func=None):
|
def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None):
|
||||||
"""
|
"""
|
||||||
Set workload status based on complete contexts.
|
Set workload status based on complete contexts.
|
||||||
status-set missing or incomplete contexts
|
status-set missing or incomplete contexts
|
||||||
and juju-log details of missing required data.
|
and juju-log details of missing required data.
|
||||||
charm_func is a charm specific function to run checking
|
charm_func is a charm specific function to run checking
|
||||||
for charm specific requirements such as a VIP setting.
|
for charm specific requirements such as a VIP setting.
|
||||||
|
|
||||||
|
This function also checks for whether the services defined are ACTUALLY
|
||||||
|
running and that the ports they advertise are open and being listened to.
|
||||||
|
|
||||||
|
@param services - OPTIONAL: a [{'service': <string>, 'ports': [<int>]]
|
||||||
|
The ports are optional.
|
||||||
|
If services is a [<string>] then ports are ignored.
|
||||||
|
@param ports - OPTIONAL: an [<int>] representing ports that shoudl be
|
||||||
|
open.
|
||||||
|
@returns None
|
||||||
"""
|
"""
|
||||||
incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
|
incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
|
||||||
state = 'active'
|
state = 'active'
|
||||||
@@ -945,6 +957,65 @@ def set_os_workload_status(configs, required_interfaces, charm_func=None):
|
|||||||
else:
|
else:
|
||||||
message = charm_message
|
message = charm_message
|
||||||
|
|
||||||
|
# If the charm thinks the unit is active, check that the actual services
|
||||||
|
# really are active.
|
||||||
|
if services is not None and state == 'active':
|
||||||
|
# if we're passed the dict() then just grab the values as a list.
|
||||||
|
if isinstance(services, dict):
|
||||||
|
services = services.values()
|
||||||
|
# either extract the list of services from the dictionary, or if
|
||||||
|
# it is a simple string, use that. i.e. works with mixed lists.
|
||||||
|
_s = []
|
||||||
|
for s in services:
|
||||||
|
if isinstance(s, dict) and 'service' in s:
|
||||||
|
_s.append(s['service'])
|
||||||
|
if isinstance(s, str):
|
||||||
|
_s.append(s)
|
||||||
|
services_running = [service_running(s) for s in _s]
|
||||||
|
if not all(services_running):
|
||||||
|
not_running = [s for s, running in zip(_s, services_running)
|
||||||
|
if not running]
|
||||||
|
message = ("Services not running that should be: {}"
|
||||||
|
.format(", ".join(not_running)))
|
||||||
|
state = 'blocked'
|
||||||
|
# also verify that the ports that should be open are open
|
||||||
|
# NB, that ServiceManager objects only OPTIONALLY have ports
|
||||||
|
port_map = OrderedDict([(s['service'], s['ports'])
|
||||||
|
for s in services if 'ports' in s])
|
||||||
|
if state == 'active' and port_map:
|
||||||
|
all_ports = list(itertools.chain(*port_map.values()))
|
||||||
|
ports_open = [port_has_listener('0.0.0.0', p)
|
||||||
|
for p in all_ports]
|
||||||
|
if not all(ports_open):
|
||||||
|
not_opened = [p for p, opened in zip(all_ports, ports_open)
|
||||||
|
if not opened]
|
||||||
|
map_not_open = OrderedDict()
|
||||||
|
for service, ports in port_map.items():
|
||||||
|
closed_ports = set(ports).intersection(not_opened)
|
||||||
|
if closed_ports:
|
||||||
|
map_not_open[service] = closed_ports
|
||||||
|
# find which service has missing ports. They are in service
|
||||||
|
# order which makes it a bit easier.
|
||||||
|
message = (
|
||||||
|
"Services with ports not open that should be: {}"
|
||||||
|
.format(
|
||||||
|
", ".join([
|
||||||
|
"{}: [{}]".format(
|
||||||
|
service,
|
||||||
|
", ".join([str(v) for v in ports]))
|
||||||
|
for service, ports in map_not_open.items()])))
|
||||||
|
state = 'blocked'
|
||||||
|
|
||||||
|
if ports is not None and state == 'active':
|
||||||
|
# and we can also check ports which we don't know the service for
|
||||||
|
ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
|
||||||
|
if not all(ports_open):
|
||||||
|
message = (
|
||||||
|
"Ports which should be open, but are not: {}"
|
||||||
|
.format(", ".join([str(p) for p, v in zip(ports, ports_open)
|
||||||
|
if not v])))
|
||||||
|
state = 'blocked'
|
||||||
|
|
||||||
# Set to active if all requirements have been met
|
# Set to active if all requirements have been met
|
||||||
if state == 'active':
|
if state == 'active':
|
||||||
message = "Unit is ready"
|
message = "Unit is ready"
|
||||||
|
@@ -1871,4 +1871,5 @@ def assess_status(configs):
|
|||||||
|
|
||||||
# set the status according to the current state of the contexts
|
# set the status according to the current state of the contexts
|
||||||
set_os_workload_status(
|
set_os_workload_status(
|
||||||
configs, REQUIRED_INTERFACES, charm_func=check_optional_relations)
|
configs, REQUIRED_INTERFACES, charm_func=check_optional_relations,
|
||||||
|
services=services(), ports=determine_ports())
|
||||||
|
@@ -758,4 +758,6 @@ class TestKeystoneUtils(CharmTestCase):
|
|||||||
set_os_workload_status.assert_called_with(
|
set_os_workload_status.assert_called_with(
|
||||||
"TEST CONFIG",
|
"TEST CONFIG",
|
||||||
utils.REQUIRED_INTERFACES,
|
utils.REQUIRED_INTERFACES,
|
||||||
charm_func=utils.check_optional_relations)
|
charm_func=utils.check_optional_relations,
|
||||||
|
services=['haproxy', 'keystone', 'apache2'],
|
||||||
|
ports=[5000, 35357])
|
||||||
|
Reference in New Issue
Block a user