Skip to content

Commit

Permalink
[tinwood, r=thedac] Add service and port checks to assess_status()
Browse files Browse the repository at this point in the history
  • Loading branch information
David Ames committed Feb 19, 2016
2 parents 65e10fa + 348d834 commit 3f4d855
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 5 deletions.
15 changes: 15 additions & 0 deletions charmhelpers/contrib/network/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,18 @@ def get_hostname(address, fqdn=True):
return result
else:
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))
6 changes: 5 additions & 1 deletion charmhelpers/contrib/openstack/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ def __call__(self):
auth_host = format_ipv6_addr(auth_host) or auth_host
svc_protocol = rdata.get('service_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'),
'service_host': serv_host,
'auth_host': auth_host,
Expand All @@ -418,7 +419,8 @@ def __call__(self):
'admin_user': rdata.get('service_username'),
'admin_password': rdata.get('service_password'),
'service_protocol': svc_protocol,
'auth_protocol': auth_protocol})
'auth_protocol': auth_protocol,
'api_version': api_version})

if self.context_complete(ctxt):
# NOTE(jamespage) this is required for >= icehouse
Expand Down Expand Up @@ -1471,6 +1473,8 @@ def __call__(self):
rdata.get('service_protocol') or 'http',
'auth_protocol':
rdata.get('auth_protocol') or 'http',
'api_version':
rdata.get('api_version') or '2.0',
}
if self.context_complete(ctxt):
return ctxt
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{% 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]
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
Expand All @@ -7,3 +17,4 @@ admin_user = {{ admin_user }}
admin_password = {{ admin_password }}
signing_dir = {{ signing_dir }}
{% endif -%}
{% endif -%}
75 changes: 73 additions & 2 deletions charmhelpers/contrib/openstack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
import sys
import re
import itertools

import six
import tempfile
Expand Down Expand Up @@ -60,14 +61,15 @@
from charmhelpers.contrib.network.ip import (
get_ipv6_addr,
is_ipv6,
port_has_listener,
)

from charmhelpers.contrib.python.packages import (
pip_create_virtualenv,
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.contrib.storage.linux.utils import is_block_device, zap_disk
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
Expand Down Expand Up @@ -860,13 +862,23 @@ def wrapped_f(*args, **kwargs):
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.
status-set missing or incomplete contexts
and juju-log details of missing required data.
charm_func is a charm specific function to run checking
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)
state = 'active'
Expand Down Expand Up @@ -945,6 +957,65 @@ def set_os_workload_status(configs, required_interfaces, charm_func=None):
else:
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
if state == 'active':
message = "Unit is ready"
Expand Down
3 changes: 2 additions & 1 deletion hooks/keystone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1871,4 +1871,5 @@ def assess_status(configs):

# set the status according to the current state of the contexts
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())
4 changes: 3 additions & 1 deletion unit_tests/test_keystone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,4 +758,6 @@ def test_assess_status(self, status_set, is_paused):
set_os_workload_status.assert_called_with(
"TEST CONFIG",
utils.REQUIRED_INTERFACES,
charm_func=utils.check_optional_relations)
charm_func=utils.check_optional_relations,
services=['haproxy', 'keystone', 'apache2'],
ports=[5000, 35357])

0 comments on commit 3f4d855

Please sign in to comment.