[jamespage, r=thedac, cholcombe] Workload Status

This commit is contained in:
David Ames 2015-10-08 13:45:26 -07:00
commit 1809f85a66
19 changed files with 278 additions and 16 deletions

View File

@ -21,7 +21,8 @@ from charmhelpers.core.hookenv import (
log,
ERROR,
WARNING,
cached
cached,
status_set,
)
from charmhelpers.contrib.storage.linux.utils import (
zap_disk,
@ -365,6 +366,7 @@ def osdize_dev(dev, osd_format, osd_journal, reformat_osd=False,
log('Looks like {} is in use, skipping.'.format(dev))
return
status_set('maintenance', 'Initializing device {}'.format(dev))
cmd = ['ceph-disk-prepare']
# Later versions of ceph support more options
if cmp_pkgrevno('ceph', '0.48.3') >= 0:

View File

@ -26,7 +26,9 @@ from charmhelpers.core.hookenv import (
remote_unit,
Hooks, UnregisteredHookError,
service_name,
relations_of_type
relations_of_type,
status_set,
local_unit,
)
from charmhelpers.core.host import (
service_restart,
@ -152,6 +154,7 @@ def config_changed():
# Support use of single node ceph
if (not ceph.is_bootstrapped() and int(config('monitor-count')) == 1):
status_set('maintenance', 'Bootstrapping single Ceph MON')
ceph.bootstrap_monitor_cluster(config('monitor-secret'))
ceph.wait_for_bootstrap()
@ -181,6 +184,20 @@ def get_mon_hosts():
return hosts
def get_peer_units():
'''
Returns a dictionary of unit names from the mon peer relation with
a flag indicating whether the unit has presented its address
'''
units = {}
units[local_unit()] = True
for relid in relation_ids('mon'):
for unit in related_units(relid):
addr = relation_get('ceph-public-address', unit, relid)
units[unit] = addr is not None
return units
def reformat_osd():
if config('osd-reformat'):
return True
@ -210,6 +227,7 @@ def mon_relation():
moncount = int(config('monitor-count'))
if len(get_mon_hosts()) >= moncount:
status_set('maintenance', 'Bootstrapping MON cluster')
ceph.bootstrap_monitor_cluster(config('monitor-secret'))
ceph.wait_for_bootstrap()
for dev in get_devices():
@ -384,8 +402,34 @@ def update_nrpe_config():
nrpe_setup.write()
def assess_status():
'''Assess status of current unit'''
moncount = int(config('monitor-count'))
units = get_peer_units()
# not enough peers and mon_count > 1
if len(units.keys()) < moncount:
status_set('blocked', 'Insufficient peer units to bootstrap'
' cluster (require {})'.format(moncount))
return
# mon_count > 1, peers, but no ceph-public-address
ready = sum(1 for unit_ready in units.itervalues() if unit_ready)
if ready < moncount:
status_set('waiting', 'Peer units detected, waiting for addresses')
return
# active - bootstrapped + quorum status check
if ceph.is_bootstrapped() and ceph.is_quorum():
status_set('active', 'Unit is ready and clustered')
else:
# Unit should be running and clustered, but no quorum
# TODO: should this be blocked or waiting?
status_set('blocked', 'Unit not clustered (no quorum)')
if __name__ == '__main__':
try:
hooks.execute(sys.argv)
except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e))
assess_status()

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

1
hooks/update-status Symbolic link
View File

@ -0,0 +1 @@
ceph_hooks.py

View File

@ -1 +1 @@
hooks.py
ceph_hooks.py

94
unit_tests/test_status.py Normal file
View File

@ -0,0 +1,94 @@
import mock
import test_utils
import ceph_hooks as hooks
TO_PATCH = [
'status_set',
'config',
'ceph',
'relation_ids',
'relation_get',
'related_units',
'local_unit',
]
NO_PEERS = {
'ceph-mon1': True
}
ENOUGH_PEERS_INCOMPLETE = {
'ceph-mon1': True,
'ceph-mon2': False,
'ceph-mon3': False,
}
ENOUGH_PEERS_COMPLETE = {
'ceph-mon1': True,
'ceph-mon2': True,
'ceph-mon3': True,
}
class ServiceStatusTestCase(test_utils.CharmTestCase):
def setUp(self):
super(ServiceStatusTestCase, self).setUp(hooks, TO_PATCH)
self.config.side_effect = self.test_config.get
self.test_config.set('monitor-count', 3)
self.local_unit.return_value = 'ceph-mon1'
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_no_peers(self, _peer_units):
_peer_units.return_value = NO_PEERS
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_incomplete(self, _peer_units):
_peer_units.return_value = ENOUGH_PEERS_INCOMPLETE
hooks.assess_status()
self.status_set.assert_called_with('waiting', mock.ANY)
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_complete_active(self, _peer_units):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
hooks.assess_status()
self.status_set.assert_called_with('active', mock.ANY)
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_complete_down(self, _peer_units):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
self.ceph.is_bootstrapped.return_value = False
self.ceph.is_quorum.return_value = False
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
def test_get_peer_units_no_peers(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = []
self.assertEquals({'ceph-mon1': True},
hooks.get_peer_units())
def test_get_peer_units_peers_incomplete(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = ['ceph-mon2',
'ceph-mon3']
self.relation_get.return_value = None
self.assertEquals({'ceph-mon1': True,
'ceph-mon2': False,
'ceph-mon3': False},
hooks.get_peer_units())
def test_get_peer_units_peers_complete(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = ['ceph-mon2',
'ceph-mon3']
self.relation_get.side_effect = ['ceph-mon2',
'ceph-mon3']
self.assertEquals({'ceph-mon1': True,
'ceph-mon2': True,
'ceph-mon3': True},
hooks.get_peer_units())

121
unit_tests/test_utils.py Normal file
View File

@ -0,0 +1,121 @@
import logging
import unittest
import os
import yaml
from contextlib import contextmanager
from mock import patch, MagicMock
def load_config():
'''
Walk backwords from __file__ looking for config.yaml, load and return the
'options' section'
'''
config = None
f = __file__
while config is None:
d = os.path.dirname(f)
if os.path.isfile(os.path.join(d, 'config.yaml')):
config = os.path.join(d, 'config.yaml')
break
f = d
if not config:
logging.error('Could not find config.yaml in any parent directory '
'of %s. ' % f)
raise Exception
return yaml.safe_load(open(config).read())['options']
def get_default_config():
'''
Load default charm config from config.yaml return as a dict.
If no default is set in config.yaml, its value is None.
'''
default_config = {}
config = load_config()
for k, v in config.iteritems():
if 'default' in v:
default_config[k] = v['default']
else:
default_config[k] = None
return default_config
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super(CharmTestCase, self).setUp()
self.patches = patches
self.obj = obj
self.test_config = TestConfig()
self.test_relation = TestRelation()
self.patch_all()
def patch(self, method):
_m = patch.object(self.obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_all(self):
for method in self.patches:
setattr(self, method, self.patch(method))
class TestConfig(object):
def __init__(self):
self.config = get_default_config()
def get(self, attr=None):
if not attr:
return self.get_all()
try:
return self.config[attr]
except KeyError:
return None
def get_all(self):
return self.config
def set(self, attr, value):
if attr not in self.config:
raise KeyError
self.config[attr] = value
class TestRelation(object):
def __init__(self, relation_data={}):
self.relation_data = relation_data
def set(self, relation_data):
self.relation_data = relation_data
def get(self, attr=None, unit=None, rid=None):
if attr is None:
return self.relation_data
elif attr in self.relation_data:
return self.relation_data[attr]
return None
@contextmanager
def patch_open():
'''Patch open() to allow mocking both open() itself and the file that is
yielded.
Yields the mock for "open" and "file", respectively.'''
mock_open = MagicMock(spec=open)
mock_file = MagicMock(spec=file)
@contextmanager
def stub_open(*args, **kwargs):
mock_open(*args, **kwargs)
yield mock_file
with patch('__builtin__.open', stub_open):
yield mock_open, mock_file