Merge lp:~james-page/charms/trusty/ceph/status into lp:~openstack-charmers-archive/charms/trusty/ceph/next

Proposed by James Page on 2015-10-07
Status: Merged
Merged at revision: 120
Proposed branch: lp:~james-page/charms/trusty/ceph/status
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph/next
Diff against target: 573 lines (+263/-102)
5 files modified
hooks/ceph.py (+3/-1)
hooks/ceph_broker.py (+0/-100)
hooks/ceph_hooks.py (+45/-1)
unit_tests/test_status.py (+94/-0)
unit_tests/test_utils.py (+121/-0)
To merge this branch: bzr merge lp:~james-page/charms/trusty/ceph/status
Reviewer Review Type Date Requested Status
David Ames Approve on 2015-10-08
James Page Resubmit on 2015-10-08
Chris Holcombe (community) 2015-10-07 Needs Fixing on 2015-10-07
Review via email: mp+273635@code.launchpad.net
To post a comment you must log in.

charm_lint_check #11479 ceph-next for james-page mp273635
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/11479/

charm_unit_test #10668 ceph-next for james-page mp273635
    UNIT FAIL: unit-test failed

UNIT Results (max last 2 lines):
make: *** [test] Error 1
ERROR:root:Make target returned non-zero.

Full unit test output: http://paste.ubuntu.com/12702241/
Build: http://10.245.162.77:8080/job/charm_unit_test/10668/

charm_amulet_test #7176 ceph-next for james-page mp273635
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/7176/

charm_unit_test #10674 ceph-next for james-page mp273635
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/10674/

Chris Holcombe (xfactor973) wrote :

Except for some tiny nits this looks great

review: Needs Fixing
James Page (james-page) wrote :

Urgh - looks like I managed to mess up something - ceph_broker.py remains unchanged, but has been removed/added to the branch for some reason.

I did have some issues running unit tests - I suspect that's the cause. If that needs some fixes, lets do it under a different MP.

review: Resubmit
David Ames (thedac) wrote :

Tests pass.
Approved, merging.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/ceph.py'
2--- hooks/ceph.py 2015-03-23 17:59:39 +0000
3+++ hooks/ceph.py 2015-10-07 01:57:34 +0000
4@@ -21,7 +21,8 @@
5 log,
6 ERROR,
7 WARNING,
8- cached
9+ cached,
10+ status_set,
11 )
12 from charmhelpers.contrib.storage.linux.utils import (
13 zap_disk,
14@@ -365,6 +366,7 @@
15 log('Looks like {} is in use, skipping.'.format(dev))
16 return
17
18+ status_set('maintenance', 'Initializing device {}'.format(dev))
19 cmd = ['ceph-disk-prepare']
20 # Later versions of ceph support more options
21 if cmp_pkgrevno('ceph', '0.48.3') >= 0:
22
23=== added file 'hooks/ceph_broker.py'
24--- hooks/ceph_broker.py 1970-01-01 00:00:00 +0000
25+++ hooks/ceph_broker.py 2015-10-07 01:57:34 +0000
26@@ -0,0 +1,100 @@
27+#!/usr/bin/python
28+#
29+# Copyright 2014 Canonical Ltd.
30+#
31+import json
32+
33+from charmhelpers.core.hookenv import (
34+ log,
35+ DEBUG,
36+ INFO,
37+ ERROR,
38+)
39+from charmhelpers.contrib.storage.linux.ceph import (
40+ create_pool,
41+ pool_exists,
42+)
43+
44+
45+def decode_req_encode_rsp(f):
46+ """Decorator to decode incoming requests and encode responses."""
47+ def decode_inner(req):
48+ return json.dumps(f(json.loads(req)))
49+
50+ return decode_inner
51+
52+
53+@decode_req_encode_rsp
54+def process_requests(reqs):
55+ """Process Ceph broker request(s).
56+
57+ This is a versioned api. API version must be supplied by the client making
58+ the request.
59+ """
60+ request_id = reqs.get('request-id')
61+ try:
62+ version = reqs.get('api-version')
63+ if version == 1:
64+ log('Processing request {}'.format(request_id), level=DEBUG)
65+ resp = process_requests_v1(reqs['ops'])
66+ if request_id:
67+ resp['request-id'] = request_id
68+
69+ return resp
70+
71+ except Exception as exc:
72+ log(str(exc), level=ERROR)
73+ msg = ("Unexpected error occurred while processing requests: %s" %
74+ (reqs))
75+ log(msg, level=ERROR)
76+ return {'exit-code': 1, 'stderr': msg}
77+
78+ msg = ("Missing or invalid api version (%s)" % (version))
79+ resp = {'exit-code': 1, 'stderr': msg}
80+ if request_id:
81+ resp['request-id'] = request_id
82+
83+ return resp
84+
85+
86+def process_requests_v1(reqs):
87+ """Process v1 requests.
88+
89+ Takes a list of requests (dicts) and processes each one. If an error is
90+ found, processing stops and the client is notified in the response.
91+
92+ Returns a response dict containing the exit code (non-zero if any
93+ operation failed along with an explanation).
94+ """
95+ log("Processing %s ceph broker requests" % (len(reqs)), level=INFO)
96+ for req in reqs:
97+ op = req.get('op')
98+ log("Processing op='%s'" % (op), level=DEBUG)
99+ # Use admin client since we do not have other client key locations
100+ # setup to use them for these operations.
101+ svc = 'admin'
102+ if op == "create-pool":
103+ params = {'pool': req.get('name'),
104+ 'replicas': req.get('replicas')}
105+ if not all(params.iteritems()):
106+ msg = ("Missing parameter(s): %s" %
107+ (' '.join([k for k in params.iterkeys()
108+ if not params[k]])))
109+ log(msg, level=ERROR)
110+ return {'exit-code': 1, 'stderr': msg}
111+
112+ pool = params['pool']
113+ replicas = params['replicas']
114+ if not pool_exists(service=svc, name=pool):
115+ log("Creating pool '%s' (replicas=%s)" % (pool, replicas),
116+ level=INFO)
117+ create_pool(service=svc, name=pool, replicas=replicas)
118+ else:
119+ log("Pool '%s' already exists - skipping create" % (pool),
120+ level=DEBUG)
121+ else:
122+ msg = "Unknown operation '%s'" % (op)
123+ log(msg, level=ERROR)
124+ return {'exit-code': 1, 'stderr': msg}
125+
126+ return {'exit-code': 0}
127
128=== removed file 'hooks/ceph_broker.py'
129--- hooks/ceph_broker.py 2015-09-04 10:33:49 +0000
130+++ hooks/ceph_broker.py 1970-01-01 00:00:00 +0000
131@@ -1,100 +0,0 @@
132-#!/usr/bin/python
133-#
134-# Copyright 2014 Canonical Ltd.
135-#
136-import json
137-
138-from charmhelpers.core.hookenv import (
139- log,
140- DEBUG,
141- INFO,
142- ERROR,
143-)
144-from charmhelpers.contrib.storage.linux.ceph import (
145- create_pool,
146- pool_exists,
147-)
148-
149-
150-def decode_req_encode_rsp(f):
151- """Decorator to decode incoming requests and encode responses."""
152- def decode_inner(req):
153- return json.dumps(f(json.loads(req)))
154-
155- return decode_inner
156-
157-
158-@decode_req_encode_rsp
159-def process_requests(reqs):
160- """Process Ceph broker request(s).
161-
162- This is a versioned api. API version must be supplied by the client making
163- the request.
164- """
165- request_id = reqs.get('request-id')
166- try:
167- version = reqs.get('api-version')
168- if version == 1:
169- log('Processing request {}'.format(request_id), level=DEBUG)
170- resp = process_requests_v1(reqs['ops'])
171- if request_id:
172- resp['request-id'] = request_id
173-
174- return resp
175-
176- except Exception as exc:
177- log(str(exc), level=ERROR)
178- msg = ("Unexpected error occurred while processing requests: %s" %
179- (reqs))
180- log(msg, level=ERROR)
181- return {'exit-code': 1, 'stderr': msg}
182-
183- msg = ("Missing or invalid api version (%s)" % (version))
184- resp = {'exit-code': 1, 'stderr': msg}
185- if request_id:
186- resp['request-id'] = request_id
187-
188- return resp
189-
190-
191-def process_requests_v1(reqs):
192- """Process v1 requests.
193-
194- Takes a list of requests (dicts) and processes each one. If an error is
195- found, processing stops and the client is notified in the response.
196-
197- Returns a response dict containing the exit code (non-zero if any
198- operation failed along with an explanation).
199- """
200- log("Processing %s ceph broker requests" % (len(reqs)), level=INFO)
201- for req in reqs:
202- op = req.get('op')
203- log("Processing op='%s'" % (op), level=DEBUG)
204- # Use admin client since we do not have other client key locations
205- # setup to use them for these operations.
206- svc = 'admin'
207- if op == "create-pool":
208- params = {'pool': req.get('name'),
209- 'replicas': req.get('replicas')}
210- if not all(params.iteritems()):
211- msg = ("Missing parameter(s): %s" %
212- (' '.join([k for k in params.iterkeys()
213- if not params[k]])))
214- log(msg, level=ERROR)
215- return {'exit-code': 1, 'stderr': msg}
216-
217- pool = params['pool']
218- replicas = params['replicas']
219- if not pool_exists(service=svc, name=pool):
220- log("Creating pool '%s' (replicas=%s)" % (pool, replicas),
221- level=INFO)
222- create_pool(service=svc, name=pool, replicas=replicas)
223- else:
224- log("Pool '%s' already exists - skipping create" % (pool),
225- level=DEBUG)
226- else:
227- msg = "Unknown operation '%s'" % (op)
228- log(msg, level=ERROR)
229- return {'exit-code': 1, 'stderr': msg}
230-
231- return {'exit-code': 0}
232
233=== renamed file 'hooks/hooks.py' => 'hooks/ceph_hooks.py'
234--- hooks/hooks.py 2015-09-22 13:35:02 +0000
235+++ hooks/ceph_hooks.py 2015-10-07 01:57:34 +0000
236@@ -26,7 +26,9 @@
237 remote_unit,
238 Hooks, UnregisteredHookError,
239 service_name,
240- relations_of_type
241+ relations_of_type,
242+ status_set,
243+ local_unit,
244 )
245 from charmhelpers.core.host import (
246 service_restart,
247@@ -152,6 +154,7 @@
248
249 # Support use of single node ceph
250 if (not ceph.is_bootstrapped() and int(config('monitor-count')) == 1):
251+ status_set('maintenance', 'Bootstrapping single Ceph MON')
252 ceph.bootstrap_monitor_cluster(config('monitor-secret'))
253 ceph.wait_for_bootstrap()
254
255@@ -181,6 +184,20 @@
256 return hosts
257
258
259+def get_peer_units():
260+ '''
261+ Returns a dictionary of unit names from the mon peer relation with
262+ a flag indicating whether the unit has presented its address
263+ '''
264+ units = {}
265+ units[local_unit()] = True
266+ for relid in relation_ids('mon'):
267+ for unit in related_units(relid):
268+ addr = relation_get('ceph-public-address', unit, relid)
269+ units[unit] = addr is not None
270+ return units
271+
272+
273 def reformat_osd():
274 if config('osd-reformat'):
275 return True
276@@ -210,6 +227,7 @@
277
278 moncount = int(config('monitor-count'))
279 if len(get_mon_hosts()) >= moncount:
280+ status_set('maintenance', 'Bootstrapping MON cluster')
281 ceph.bootstrap_monitor_cluster(config('monitor-secret'))
282 ceph.wait_for_bootstrap()
283 for dev in get_devices():
284@@ -384,8 +402,34 @@
285 nrpe_setup.write()
286
287
288+def assess_status():
289+ '''Assess status of current unit'''
290+ moncount = int(config('monitor-count'))
291+ units = get_peer_units()
292+ # not enough peers and mon_count > 1
293+ if len(units.keys()) < moncount:
294+ status_set('blocked', 'Insufficient peer units to bootstrap'
295+ ' cluster (require {})'.format(moncount))
296+ return
297+
298+ # mon_count > 1, peers, but no ceph-public-address
299+ ready = sum(1 for unit_ready in units.itervalues() if unit_ready)
300+ if ready < moncount:
301+ status_set('waiting', 'Peer units detected, waiting for addresses')
302+ return
303+
304+ # active - bootstrapped + quorum status check
305+ if ceph.is_bootstrapped() and ceph.is_quorum():
306+ status_set('active', 'Unit is ready and clustered')
307+ else:
308+ # Unit should be running and clustered, but no quorum
309+ # TODO: should this be blocked or waiting?
310+ status_set('blocked', 'Unit not clustered (no quorum)')
311+
312+
313 if __name__ == '__main__':
314 try:
315 hooks.execute(sys.argv)
316 except UnregisteredHookError as e:
317 log('Unknown hook {} - skipping.'.format(e))
318+ assess_status()
319
320=== modified symlink 'hooks/client-relation-changed'
321=== target changed u'hooks.py' => u'ceph_hooks.py'
322=== modified symlink 'hooks/client-relation-joined'
323=== target changed u'hooks.py' => u'ceph_hooks.py'
324=== modified symlink 'hooks/config-changed'
325=== target changed u'hooks.py' => u'ceph_hooks.py'
326=== modified symlink 'hooks/install.real'
327=== target changed u'hooks.py' => u'ceph_hooks.py'
328=== modified symlink 'hooks/mon-relation-changed'
329=== target changed u'hooks.py' => u'ceph_hooks.py'
330=== modified symlink 'hooks/mon-relation-departed'
331=== target changed u'hooks.py' => u'ceph_hooks.py'
332=== modified symlink 'hooks/mon-relation-joined'
333=== target changed u'hooks.py' => u'ceph_hooks.py'
334=== modified symlink 'hooks/nrpe-external-master-relation-changed'
335=== target changed u'hooks.py' => u'ceph_hooks.py'
336=== modified symlink 'hooks/nrpe-external-master-relation-joined'
337=== target changed u'hooks.py' => u'ceph_hooks.py'
338=== modified symlink 'hooks/osd-relation-joined'
339=== target changed u'hooks.py' => u'ceph_hooks.py'
340=== modified symlink 'hooks/radosgw-relation-joined'
341=== target changed u'hooks.py' => u'ceph_hooks.py'
342=== modified symlink 'hooks/start'
343=== target changed u'hooks.py' => u'ceph_hooks.py'
344=== modified symlink 'hooks/stop'
345=== target changed u'hooks.py' => u'ceph_hooks.py'
346=== added symlink 'hooks/update-status'
347=== target is u'ceph_hooks.py'
348=== modified symlink 'hooks/upgrade-charm'
349=== target changed u'hooks.py' => u'ceph_hooks.py'
350=== added file 'unit_tests/test_status.py'
351--- unit_tests/test_status.py 1970-01-01 00:00:00 +0000
352+++ unit_tests/test_status.py 2015-10-07 01:57:34 +0000
353@@ -0,0 +1,94 @@
354+import mock
355+import test_utils
356+
357+import ceph_hooks as hooks
358+
359+TO_PATCH = [
360+ 'status_set',
361+ 'config',
362+ 'ceph',
363+ 'relation_ids',
364+ 'relation_get',
365+ 'related_units',
366+ 'local_unit',
367+]
368+
369+NO_PEERS = {
370+ 'ceph-mon1': True
371+}
372+
373+ENOUGH_PEERS_INCOMPLETE = {
374+ 'ceph-mon1': True,
375+ 'ceph-mon2': False,
376+ 'ceph-mon3': False,
377+}
378+
379+ENOUGH_PEERS_COMPLETE = {
380+ 'ceph-mon1': True,
381+ 'ceph-mon2': True,
382+ 'ceph-mon3': True,
383+}
384+
385+
386+class ServiceStatusTestCase(test_utils.CharmTestCase):
387+
388+ def setUp(self):
389+ super(ServiceStatusTestCase, self).setUp(hooks, TO_PATCH)
390+ self.config.side_effect = self.test_config.get
391+ self.test_config.set('monitor-count', 3)
392+ self.local_unit.return_value = 'ceph-mon1'
393+
394+ @mock.patch.object(hooks, 'get_peer_units')
395+ def test_assess_status_no_peers(self, _peer_units):
396+ _peer_units.return_value = NO_PEERS
397+ hooks.assess_status()
398+ self.status_set.assert_called_with('blocked', mock.ANY)
399+
400+ @mock.patch.object(hooks, 'get_peer_units')
401+ def test_assess_status_peers_incomplete(self, _peer_units):
402+ _peer_units.return_value = ENOUGH_PEERS_INCOMPLETE
403+ hooks.assess_status()
404+ self.status_set.assert_called_with('waiting', mock.ANY)
405+
406+ @mock.patch.object(hooks, 'get_peer_units')
407+ def test_assess_status_peers_complete_active(self, _peer_units):
408+ _peer_units.return_value = ENOUGH_PEERS_COMPLETE
409+ self.ceph.is_bootstrapped.return_value = True
410+ self.ceph.is_quorum.return_value = True
411+ hooks.assess_status()
412+ self.status_set.assert_called_with('active', mock.ANY)
413+
414+ @mock.patch.object(hooks, 'get_peer_units')
415+ def test_assess_status_peers_complete_down(self, _peer_units):
416+ _peer_units.return_value = ENOUGH_PEERS_COMPLETE
417+ self.ceph.is_bootstrapped.return_value = False
418+ self.ceph.is_quorum.return_value = False
419+ hooks.assess_status()
420+ self.status_set.assert_called_with('blocked', mock.ANY)
421+
422+ def test_get_peer_units_no_peers(self):
423+ self.relation_ids.return_value = ['mon:1']
424+ self.related_units.return_value = []
425+ self.assertEquals({'ceph-mon1': True},
426+ hooks.get_peer_units())
427+
428+ def test_get_peer_units_peers_incomplete(self):
429+ self.relation_ids.return_value = ['mon:1']
430+ self.related_units.return_value = ['ceph-mon2',
431+ 'ceph-mon3']
432+ self.relation_get.return_value = None
433+ self.assertEquals({'ceph-mon1': True,
434+ 'ceph-mon2': False,
435+ 'ceph-mon3': False},
436+ hooks.get_peer_units())
437+
438+ def test_get_peer_units_peers_complete(self):
439+ self.relation_ids.return_value = ['mon:1']
440+ self.related_units.return_value = ['ceph-mon2',
441+ 'ceph-mon3']
442+ self.relation_get.side_effect = ['ceph-mon2',
443+ 'ceph-mon3']
444+ self.assertEquals({'ceph-mon1': True,
445+ 'ceph-mon2': True,
446+ 'ceph-mon3': True},
447+ hooks.get_peer_units())
448
449=== added file 'unit_tests/test_utils.py'
450--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
451+++ unit_tests/test_utils.py 2015-10-07 01:57:34 +0000
452@@ -0,0 +1,121 @@
453+import logging
454+import unittest
455+import os
456+import yaml
457+
458+from contextlib import contextmanager
459+from mock import patch, MagicMock
460+
461+
462+def load_config():
463+ '''
464+ Walk backwords from __file__ looking for config.yaml, load and return the
465+ 'options' section'
466+ '''
467+ config = None
468+ f = __file__
469+ while config is None:
470+ d = os.path.dirname(f)
471+ if os.path.isfile(os.path.join(d, 'config.yaml')):
472+ config = os.path.join(d, 'config.yaml')
473+ break
474+ f = d
475+
476+ if not config:
477+ logging.error('Could not find config.yaml in any parent directory '
478+ 'of %s. ' % f)
479+ raise Exception
480+
481+ return yaml.safe_load(open(config).read())['options']
482+
483+
484+def get_default_config():
485+ '''
486+ Load default charm config from config.yaml return as a dict.
487+ If no default is set in config.yaml, its value is None.
488+ '''
489+ default_config = {}
490+ config = load_config()
491+ for k, v in config.iteritems():
492+ if 'default' in v:
493+ default_config[k] = v['default']
494+ else:
495+ default_config[k] = None
496+ return default_config
497+
498+
499+class CharmTestCase(unittest.TestCase):
500+
501+ def setUp(self, obj, patches):
502+ super(CharmTestCase, self).setUp()
503+ self.patches = patches
504+ self.obj = obj
505+ self.test_config = TestConfig()
506+ self.test_relation = TestRelation()
507+ self.patch_all()
508+
509+ def patch(self, method):
510+ _m = patch.object(self.obj, method)
511+ mock = _m.start()
512+ self.addCleanup(_m.stop)
513+ return mock
514+
515+ def patch_all(self):
516+ for method in self.patches:
517+ setattr(self, method, self.patch(method))
518+
519+
520+class TestConfig(object):
521+
522+ def __init__(self):
523+ self.config = get_default_config()
524+
525+ def get(self, attr=None):
526+ if not attr:
527+ return self.get_all()
528+ try:
529+ return self.config[attr]
530+ except KeyError:
531+ return None
532+
533+ def get_all(self):
534+ return self.config
535+
536+ def set(self, attr, value):
537+ if attr not in self.config:
538+ raise KeyError
539+ self.config[attr] = value
540+
541+
542+class TestRelation(object):
543+
544+ def __init__(self, relation_data={}):
545+ self.relation_data = relation_data
546+
547+ def set(self, relation_data):
548+ self.relation_data = relation_data
549+
550+ def get(self, attr=None, unit=None, rid=None):
551+ if attr is None:
552+ return self.relation_data
553+ elif attr in self.relation_data:
554+ return self.relation_data[attr]
555+ return None
556+
557+
558+@contextmanager
559+def patch_open():
560+ '''Patch open() to allow mocking both open() itself and the file that is
561+ yielded.
562+
563+ Yields the mock for "open" and "file", respectively.'''
564+ mock_open = MagicMock(spec=open)
565+ mock_file = MagicMock(spec=file)
566+
567+ @contextmanager
568+ def stub_open(*args, **kwargs):
569+ mock_open(*args, **kwargs)
570+ yield mock_file
571+
572+ with patch('__builtin__.open', stub_open):
573+ yield mock_open, mock_file

Subscribers

People subscribed via source and target branches