Merge lp:~ajkavanagh/charm-helpers/add-service-checks-lp1524388 into lp:charm-helpers

Proposed by Alex Kavanagh
Status: Merged
Merged at revision: 531
Proposed branch: lp:~ajkavanagh/charm-helpers/add-service-checks-lp1524388
Merge into: lp:charm-helpers
Diff against target: 329 lines (+256/-2)
4 files modified
charmhelpers/contrib/network/ip.py (+15/-0)
charmhelpers/contrib/openstack/utils.py (+73/-2)
tests/contrib/network/test_ip.py (+9/-0)
tests/contrib/openstack/test_openstack_utils.py (+159/-0)
To merge this branch: bzr merge lp:~ajkavanagh/charm-helpers/add-service-checks-lp1524388
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
David Ames (community) Approve
Review via email: mp+285604@code.launchpad.net

Description of the change

  Add service running and ports open checks to set_os_workload_status()
  This adds optional checks to the set_os_workload_status(...) function to
  make sure that the services that the charm says should be running are
  actually running (according to service_running()) and also, optionally,
  that the ports that are supposed to be open are open (on 0.0.0.0) and
  have something listening to them.

To post a comment you must log in.
Revision history for this message
David Ames (thedac) wrote :

Needs fixing:

We are going to want to set the state to 'blocked' which means user action is required. The state 'unknown' is reserved for before a state has been declared or for charms which never set state.

Hacking around a bit with your keystone example (I had to add servcies to the set_os_workload_status). When I used a simple ['keystone'] it fails:

    # set the status according to the current state of the contexts
    set_os_workload_status(
        configs, REQUIRED_INTERFACES, charm_func=check_optional_relations,
        services=['keystone'])

Traceback (most recent call last):
  File "hooks/update-status", line 654, in <module>
    main()
  File "hooks/update-status", line 650, in main
    assess_status(CONFIGS)
  File "/var/lib/juju/agents/unit-keystone-0/charm/hooks/keystone_utils.py", line 1845, in assess_status
    services=['keystone'])
  File "/var/lib/juju/agents/unit-keystone-0/charm/hooks/charmhelpers/contrib/openstack/utils.py", line 936, in set_os_workload_status
    _s = [s['service'] for s in services]
TypeError: string indices must be integers, not str

Using the full dictionary works beautifully.
    set_os_workload_status(
        configs, REQUIRED_INTERFACES, charm_func=check_optional_relations,
        services=[{'service': 'keystone', 'ports': [5000, 4990, 35357, 35347]}])

For discussion:

We need to have a discussion about the default ports for a service vrs our haproxy with non-default ports for the service. For example keystone's default is 5000 which haproxy listens for but our charm has the keystone service itself listen on 4990.

Is there anyway we can automatically "guess" these?

Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

I've updated charm-helpers to support the simple list of services (oversight, apologies), added tests, and also updated the keystone example branch (lp:~ajkavanagh/charms/trusty/keystone/add-service-checks-lp1524388) to use the new version.

I've also changed the set_os_workload_status(...) call in assess_status(...) to be:

    # set the status according to the current state of the contexts
    set_os_workload_status(
        configs, REQUIRED_INTERFACES, charm_func=check_optional_relations,
        services=services(), ports=determine_ports())

The utils function services() and determine_ports() 'appear' (for keystone, at least) the list of running services and the ports it manages. Is this true, and thus, does it answer the question you posed for discussion?

Revision history for this message
David Ames (thedac) wrote :

This looks good. Requesting a merge.

review: Approve
Revision history for this message
Liam Young (gnuoy) wrote :

Approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/network/ip.py'
2--- charmhelpers/contrib/network/ip.py 2015-12-14 10:43:51 +0000
3+++ charmhelpers/contrib/network/ip.py 2016-02-12 11:00:52 +0000
4@@ -456,3 +456,18 @@
5 return result
6 else:
7 return result.split('.')[0]
8+
9+
10+def port_has_listener(address, port):
11+ """
12+ Returns True if the address:port is open and being listened to,
13+ else False.
14+
15+ @param address: an IP address or hostname
16+ @param port: integer port
17+
18+ Note calls 'zc' via a subprocess shell
19+ """
20+ cmd = ['nc', '-z', address, str(port)]
21+ result = subprocess.call(cmd)
22+ return not(bool(result))
23
24=== modified file 'charmhelpers/contrib/openstack/utils.py'
25--- charmhelpers/contrib/openstack/utils.py 2016-02-11 15:34:00 +0000
26+++ charmhelpers/contrib/openstack/utils.py 2016-02-12 11:00:52 +0000
27@@ -23,6 +23,7 @@
28 import os
29 import sys
30 import re
31+import itertools
32
33 import six
34 import tempfile
35@@ -60,6 +61,7 @@
36 from charmhelpers.contrib.network.ip import (
37 get_ipv6_addr,
38 is_ipv6,
39+ port_has_listener,
40 )
41
42 from charmhelpers.contrib.python.packages import (
43@@ -67,7 +69,7 @@
44 pip_install,
45 )
46
47-from charmhelpers.core.host import lsb_release, mounts, umount
48+from charmhelpers.core.host import lsb_release, mounts, umount, service_running
49 from charmhelpers.fetch import apt_install, apt_cache, install_remote
50 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
51 from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
52@@ -860,13 +862,23 @@
53 return wrap
54
55
56-def set_os_workload_status(configs, required_interfaces, charm_func=None):
57+def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None):
58 """
59 Set workload status based on complete contexts.
60 status-set missing or incomplete contexts
61 and juju-log details of missing required data.
62 charm_func is a charm specific function to run checking
63 for charm specific requirements such as a VIP setting.
64+
65+ This function also checks for whether the services defined are ACTUALLY
66+ running and that the ports they advertise are open and being listened to.
67+
68+ @param services - OPTIONAL: a [{'service': <string>, 'ports': [<int>]]
69+ The ports are optional.
70+ If services is a [<string>] then ports are ignored.
71+ @param ports - OPTIONAL: an [<int>] representing ports that shoudl be
72+ open.
73+ @returns None
74 """
75 incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
76 state = 'active'
77@@ -945,6 +957,65 @@
78 else:
79 message = charm_message
80
81+ # If the charm thinks the unit is active, check that the actual services
82+ # really are active.
83+ if services is not None and state == 'active':
84+ # if we're passed the dict() then just grab the values as a list.
85+ if isinstance(services, dict):
86+ services = services.values()
87+ # either extract the list of services from the dictionary, or if
88+ # it is a simple string, use that. i.e. works with mixed lists.
89+ _s = []
90+ for s in services:
91+ if isinstance(s, dict) and 'service' in s:
92+ _s.append(s['service'])
93+ if isinstance(s, str):
94+ _s.append(s)
95+ services_running = [service_running(s) for s in _s]
96+ if not all(services_running):
97+ not_running = [s for s, running in zip(_s, services_running)
98+ if not running]
99+ message = ("Services not running that should be: {}"
100+ .format(", ".join(not_running)))
101+ state = 'blocked'
102+ # also verify that the ports that should be open are open
103+ # NB, that ServiceManager objects only OPTIONALLY have ports
104+ port_map = OrderedDict([(s['service'], s['ports'])
105+ for s in services if 'ports' in s])
106+ if state == 'active' and port_map:
107+ all_ports = list(itertools.chain(*port_map.values()))
108+ ports_open = [port_has_listener('0.0.0.0', p)
109+ for p in all_ports]
110+ if not all(ports_open):
111+ not_opened = [p for p, opened in zip(all_ports, ports_open)
112+ if not opened]
113+ map_not_open = OrderedDict()
114+ for service, ports in port_map.items():
115+ closed_ports = set(ports).intersection(not_opened)
116+ if closed_ports:
117+ map_not_open[service] = closed_ports
118+ # find which service has missing ports. They are in service
119+ # order which makes it a bit easier.
120+ message = (
121+ "Services with ports not open that should be: {}"
122+ .format(
123+ ", ".join([
124+ "{}: [{}]".format(
125+ service,
126+ ", ".join([str(v) for v in ports]))
127+ for service, ports in map_not_open.items()])))
128+ state = 'blocked'
129+
130+ if ports is not None and state == 'active':
131+ # and we can also check ports which we don't know the service for
132+ ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
133+ if not all(ports_open):
134+ message = (
135+ "Ports which should be open, but are not: {}"
136+ .format(", ".join([str(p) for p, v in zip(ports, ports_open)
137+ if not v])))
138+ state = 'blocked'
139+
140 # Set to active if all requirements have been met
141 if state == 'active':
142 message = "Unit is ready"
143
144=== modified file 'tests/contrib/network/test_ip.py'
145--- tests/contrib/network/test_ip.py 2015-12-14 15:17:42 +0000
146+++ tests/contrib/network/test_ip.py 2016-02-12 11:00:52 +0000
147@@ -703,3 +703,12 @@
148 with patch(builtin_import, side_effect=[fake_dns]):
149 hn = net_ip.get_hostname('4.2.2.1')
150 self.assertEquals(hn, "www.ubuntu.com")
151+
152+ @patch('charmhelpers.contrib.network.ip.subprocess.call')
153+ def test_port_has_listener(self, subprocess_call):
154+ subprocess_call.return_value = 1
155+ self.assertEqual(net_ip.port_has_listener('ip-address', 50), False)
156+ subprocess_call.assert_called_with(['nc', '-z', 'ip-address', '50'])
157+ subprocess_call.return_value = 0
158+ self.assertEqual(net_ip.port_has_listener('ip-address', 70), True)
159+ subprocess_call.assert_called_with(['nc', '-z', 'ip-address', '70'])
160
161=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
162--- tests/contrib/openstack/test_openstack_utils.py 2016-02-11 15:34:00 +0000
163+++ tests/contrib/openstack/test_openstack_utils.py 2016-02-12 11:00:52 +0000
164@@ -1021,6 +1021,165 @@
165 self.assertTrue(actual_parm1 == 'blocked')
166 self.assertTrue(actual_parm2 == expected1 or actual_parm2 == expected2)
167
168+ @patch('charmhelpers.contrib.openstack.utils.service_running')
169+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
170+ @patch.object(openstack, 'juju_log')
171+ @patch('charmhelpers.contrib.openstack.utils.status_set')
172+ def test_set_os_workload_status_complete_with_services_list(
173+ self, status_set, log, port_has_listener, service_running):
174+ configs = MagicMock()
175+ configs.complete_contexts.return_value = ['shared-db',
176+ 'amqp',
177+ 'identity-service']
178+ required_interfaces = {
179+ 'database': ['shared-db', 'pgsql-db'],
180+ 'message': ['amqp', 'zeromq-configuration'],
181+ 'identity': ['identity-service']}
182+
183+ services = ['database', 'identity']
184+ # Assume that the service and ports are open.
185+ port_has_listener.return_value = True
186+ service_running.return_value = True
187+
188+ openstack.set_os_workload_status(
189+ configs, required_interfaces, services=services)
190+ status_set.assert_called_with('active', 'Unit is ready')
191+
192+ @patch('charmhelpers.contrib.openstack.utils.service_running')
193+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
194+ @patch.object(openstack, 'juju_log')
195+ @patch('charmhelpers.contrib.openstack.utils.status_set')
196+ def test_set_os_workload_status_complete_services_list_not_running(
197+ self, status_set, log, port_has_listener, service_running):
198+ configs = MagicMock()
199+ configs.complete_contexts.return_value = ['shared-db',
200+ 'amqp',
201+ 'identity-service']
202+ required_interfaces = {
203+ 'database': ['shared-db', 'pgsql-db'],
204+ 'message': ['amqp', 'zeromq-configuration'],
205+ 'identity': ['identity-service']}
206+
207+ services = ['database', 'identity']
208+ port_has_listener.return_value = True
209+ # Fail the identity service
210+ service_running.side_effect = [True, False]
211+
212+ openstack.set_os_workload_status(
213+ configs, required_interfaces, services=services)
214+ status_set.assert_called_with(
215+ 'blocked',
216+ 'Services not running that should be: identity')
217+
218+ @patch('charmhelpers.contrib.openstack.utils.service_running')
219+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
220+ @patch.object(openstack, 'juju_log')
221+ @patch('charmhelpers.contrib.openstack.utils.status_set')
222+ def test_set_os_workload_status_complete_with_services(
223+ self, status_set, log, port_has_listener, service_running):
224+ configs = MagicMock()
225+ configs.complete_contexts.return_value = ['shared-db',
226+ 'amqp',
227+ 'identity-service']
228+ required_interfaces = {
229+ 'database': ['shared-db', 'pgsql-db'],
230+ 'message': ['amqp', 'zeromq-configuration'],
231+ 'identity': ['identity-service']}
232+
233+ services = [
234+ {'service': 'database', 'ports': [10, 20]},
235+ {'service': 'identity', 'ports': [30]},
236+ ]
237+ # Assume that the service and ports are open.
238+ port_has_listener.return_value = True
239+ service_running.return_value = True
240+
241+ openstack.set_os_workload_status(
242+ configs, required_interfaces, services=services)
243+ status_set.assert_called_with('active', 'Unit is ready')
244+
245+ @patch('charmhelpers.contrib.openstack.utils.service_running')
246+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
247+ @patch.object(openstack, 'juju_log')
248+ @patch('charmhelpers.contrib.openstack.utils.status_set')
249+ def test_set_os_workload_status_complete_service_not_running(
250+ self, status_set, log, port_has_listener, service_running):
251+ configs = MagicMock()
252+ configs.complete_contexts.return_value = ['shared-db',
253+ 'amqp',
254+ 'identity-service']
255+ required_interfaces = {
256+ 'database': ['shared-db', 'pgsql-db'],
257+ 'message': ['amqp', 'zeromq-configuration'],
258+ 'identity': ['identity-service']}
259+
260+ services = [
261+ {'service': 'database', 'ports': [10, 20]},
262+ {'service': 'identity', 'ports': [30]},
263+ ]
264+ port_has_listener.return_value = True
265+ # Fail the identity service
266+ service_running.side_effect = [True, False]
267+
268+ openstack.set_os_workload_status(
269+ configs, required_interfaces, services=services)
270+ status_set.assert_called_with(
271+ 'blocked',
272+ 'Services not running that should be: identity')
273+
274+ @patch('charmhelpers.contrib.openstack.utils.service_running')
275+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
276+ @patch.object(openstack, 'juju_log')
277+ @patch('charmhelpers.contrib.openstack.utils.status_set')
278+ def test_set_os_workload_status_complete_port_not_open(
279+ self, status_set, log, port_has_listener, service_running):
280+ configs = MagicMock()
281+ configs.complete_contexts.return_value = ['shared-db',
282+ 'amqp',
283+ 'identity-service']
284+ required_interfaces = {
285+ 'database': ['shared-db', 'pgsql-db'],
286+ 'message': ['amqp', 'zeromq-configuration'],
287+ 'identity': ['identity-service']}
288+
289+ services = [
290+ {'service': 'database', 'ports': [10, 20]},
291+ {'service': 'identity', 'ports': [30]},
292+ ]
293+ port_has_listener.side_effect = [True, False, True]
294+ # Fail the identity service
295+ service_running.return_value = True
296+
297+ openstack.set_os_workload_status(
298+ configs, required_interfaces, services=services)
299+ status_set.assert_called_with(
300+ 'blocked',
301+ 'Services with ports not open that should be:'
302+ ' database: [20]')
303+
304+ @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
305+ @patch.object(openstack, 'juju_log')
306+ @patch('charmhelpers.contrib.openstack.utils.status_set')
307+ def test_set_os_workload_status_complete_ports_not_open(
308+ self, status_set, log, port_has_listener):
309+ configs = MagicMock()
310+ configs.complete_contexts.return_value = ['shared-db',
311+ 'amqp',
312+ 'identity-service']
313+ required_interfaces = {
314+ 'database': ['shared-db', 'pgsql-db'],
315+ 'message': ['amqp', 'zeromq-configuration'],
316+ 'identity': ['identity-service']}
317+
318+ ports = [50, 60, 70]
319+ port_has_listener.side_effect = [True, False, True]
320+
321+ openstack.set_os_workload_status(
322+ configs, required_interfaces, ports=ports)
323+ status_set.assert_called_with(
324+ 'blocked',
325+ 'Ports which should be open, but are not: 60')
326+
327 @patch.object(openstack, 'juju_log')
328 @patch.object(openstack, 'action_set')
329 @patch.object(openstack, 'action_fail')

Subscribers

People subscribed via source and target branches