Brad Crittenden has proposed merging lp:~yellow/charm-tools/trunk into lp:charm-tools.
Requested reviews: charmers (charmers) For more details, see: https://code.launchpad.net/~yellow/charm-tools/trunk/+merge/101554 Update parsing of 'juju status' output to account for change in tokens ('state' -> 'agent-state'). -- https://code.launchpad.net/~yellow/charm-tools/trunk/+merge/101554 Your team Launchpad Yellow Squad is subscribed to branch lp:~yellow/charm-tools/trunk.
=== modified file 'Makefile' --- Makefile 2012-01-03 18:19:56 +0000 +++ Makefile 2012-04-11 13:26:47 +0000 @@ -25,3 +25,4 @@ tests/helpers/helpers.sh || sh -x tests/helpers/helpers.sh timeout @echo Test shell helpers with bash bash tests/helpers/helpers.sh || bash -x tests/helpers/helpers.sh timeout + python helpers/python/charmhelpers/tests/test_charmhelpers.py === added file 'ez_setup.py' --- ez_setup.py 1970-01-01 00:00:00 +0000 +++ ez_setup.py 2012-04-11 13:26:47 +0000 @@ -0,0 +1,288 @@ +#!python + +# NOTE TO LAUNCHPAD DEVELOPERS: This is a bootstrapping file from the +# setuptools project. It is imported by our setup.py. + +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c11" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', + 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', + 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', + 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', + 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', + 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', + 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', + 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + + === added directory 'helpers/python' === added directory 'helpers/python/charmhelpers' === added file 'helpers/python/charmhelpers/__init__.py' --- helpers/python/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 +++ helpers/python/charmhelpers/__init__.py 2012-04-11 13:26:47 +0000 @@ -0,0 +1,185 @@ +# Copyright 2012 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Helper functions for writing Juju charms in Python.""" + +__metaclass__ = type +__all__ = [ + 'get_config', + 'log', + 'log_entry', + 'log_exit', + 'relation_get', + 'relation_set', + 'unit_info', + 'wait_for_machine', + 'wait_for_page_contents', + 'wait_for_relation', + 'wait_for_unit', + ] + +from collections import namedtuple +import json +import operator +from shelltoolbox import ( + command, + script_name, + ) +import tempfile +import time +import urllib2 +import yaml + + +SLEEP_AMOUNT = 0.1 +Env = namedtuple('Env', 'uid gid home') +log = command('juju-log') +# We create a juju_status Command here because it makes testing much, +# much easier. +juju_status = lambda: command('juju')('status') + + +def log_entry(): + log("--> Entering {}".format(script_name())) + + +def log_exit(): + log("<-- Exiting {}".format(script_name())) + + +def get_config(): + config_get = command('config-get', '--format=json') + return json.loads(config_get()) + + +def relation_get(*args): + cmd = command('relation-get') + return cmd(*args).strip() + + +def relation_set(**kwargs): + cmd = command('relation-set') + args = ['{}={}'.format(k, v) for k, v in kwargs.items()] + return cmd(*args) + + +def make_charm_config_file(charm_config): + charm_config_file = tempfile.NamedTemporaryFile() + charm_config_file.write(yaml.dump(charm_config)) + charm_config_file.flush() + # The NamedTemporaryFile instance is returned instead of just the name + # because we want to take advantage of garbage collection-triggered + # deletion of the temp file when it goes out of scope in the caller. + return charm_config_file + + +def unit_info(service_name, item_name, data=None, unit=None): + if data is None: + data = yaml.safe_load(juju_status()) + service = data['services'].get(service_name) + if service is None: + # XXX 2012-02-08 gmb: + # This allows us to cope with the race condition that we + # have between deploying a service and having it come up in + # `juju status`. We could probably do with cleaning it up so + # that it fails a bit more noisily after a while. + return '' + units = service['units'] + if unit is not None: + item = units[unit][item_name] + else: + # It might seem odd to sort the units here, but we do it to + # ensure that when no unit is specified, the first unit for the + # service (or at least the one with the lowest number) is the + # one whose data gets returned. + sorted_unit_names = sorted(units.keys()) + item = units[sorted_unit_names[0]][item_name] + return item + + +def get_machine_data(): + return yaml.safe_load(juju_status())['machines'] + + +def wait_for_machine(num_machines=1, timeout=300): + """Wait `timeout` seconds for `num_machines` machines to come up. + + This wait_for... function can be called by other wait_for functions + whose timeouts might be too short in situations where only a bare + Juju setup has been bootstrapped. + + :return: A tuple of (num_machines, time_taken). This is used for + testing. + """ + # You may think this is a hack, and you'd be right. The easiest way + # to tell what environment we're working in (LXC vs EC2) is to check + # the dns-name of the first machine. If it's localhost we're in LXC + # and we can just return here. + if get_machine_data()[0]['dns-name'] == 'localhost': + return 1, 0 + start_time = time.time() + while True: + # Drop the first machine, since it's the Zookeeper and that's + # not a machine that we need to wait for. This will only work + # for EC2 environments, which is why we return early above if + # we're in LXC. + machine_data = get_machine_data() + non_zookeeper_machines = [ + machine_data[key] for key in machine_data.keys()[1:]] + if len(non_zookeeper_machines) >= num_machines: + all_machines_running = True + for machine in non_zookeeper_machines: + if machine.get('instance-state') != 'running': + all_machines_running = False + break + if all_machines_running: + break + if time.time() - start_time >= timeout: + raise RuntimeError('timeout waiting for service to start') + time.sleep(SLEEP_AMOUNT) + return num_machines, time.time() - start_time + + +def wait_for_unit(service_name, timeout=480): + """Wait `timeout` seconds for a given service name to come up.""" + wait_for_machine(num_machines=1) + start_time = time.time() + while True: + state = unit_info(service_name, 'agent-state') + if 'error' in state or state == 'started': + break + if time.time() - start_time >= timeout: + raise RuntimeError('timeout waiting for service to start') + time.sleep(SLEEP_AMOUNT) + if state != 'started': + raise RuntimeError('unit did not start, agent-state: ' + state) + + +def wait_for_relation(service_name, relation_name, timeout=120): + """Wait `timeout` seconds for a given relation to come up.""" + start_time = time.time() + while True: + relation = unit_info(service_name, 'relations').get(relation_name) + if relation is not None and relation['state'] == 'up': + break + if time.time() - start_time >= timeout: + raise RuntimeError('timeout waiting for relation to be up') + time.sleep(SLEEP_AMOUNT) + + +def wait_for_page_contents(url, contents, timeout=120, validate=None): + if validate is None: + validate = operator.contains + start_time = time.time() + while True: + try: + stream = urllib2.urlopen(url) + except (urllib2.HTTPError, urllib2.URLError): + pass + else: + page = stream.read() + if validate(page, contents): + return page + if time.time() - start_time >= timeout: + raise RuntimeError('timeout waiting for contents of ' + url) + time.sleep(SLEEP_AMOUNT) === added directory 'helpers/python/charmhelpers/tests' === added file 'helpers/python/charmhelpers/tests/test_charmhelpers.py' --- helpers/python/charmhelpers/tests/test_charmhelpers.py 1970-01-01 00:00:00 +0000 +++ helpers/python/charmhelpers/tests/test_charmhelpers.py 2012-04-11 13:26:47 +0000 @@ -0,0 +1,337 @@ +# Tests for Python charm helpers. + +import unittest +import yaml + +from simplejson import dumps +from StringIO import StringIO +from testtools import TestCase + +import sys +# Path hack to ensure we test the local code, not a version installed in +# /usr/local/lib. This is necessary since /usr/local/lib is prepended before +# what is specified in PYTHONPATH. +sys.path.insert(0, 'helpers/python') +import charmhelpers + + +class CharmHelpersTestCase(TestCase): + """A basic test case for Python charm helpers.""" + + def _patch_command(self, replacement_command): + """Monkeypatch charmhelpers.command for testing purposes. + + :param replacement_command: The replacement Callable for + command(). + """ + new_command = lambda *args: replacement_command + self.patch(charmhelpers, 'command', new_command) + + def _make_juju_status_dict(self, num_units=1, + service_name='test-service', + unit_state='pending', + machine_state='not-started'): + """Generate valid juju status dict and return it.""" + machine_data = {} + # The 0th machine is the Zookeeper. + machine_data[0] = { + 'dns-name': 'zookeeper.example.com', + 'instance-id': 'machine0', + 'state': 'not-started', + } + service_data = { + 'charm': 'local:precise/{}-1'.format(service_name), + 'relations': {}, + 'units': {}, + } + for i in range(num_units): + # The machine is always going to be i+1 because there + # will always be num_units+1 machines. + machine_number = i+1 + unit_machine_data = { + 'dns-name': 'machine{}.example.com'.format(machine_number), + 'instance-id': 'machine{}'.format(machine_number), + 'state': machine_state, + 'instance-state': machine_state, + } + machine_data[machine_number] = unit_machine_data + unit_data = { + 'machine': machine_number, + 'public-address': + '{}-{}.example.com'.format(service_name, i), + 'relations': { + 'db': {'state': 'up'}, + }, + 'agent-state': unit_state, + } + service_data['units']['{}/{}'.format(service_name, i)] = ( + unit_data) + juju_status_data = { + 'machines': machine_data, + 'services': {service_name: service_data}, + } + return juju_status_data + + def _make_juju_status_yaml(self, num_units=1, + service_name='test-service', + unit_state='pending', + machine_state='not-started'): + """Convert the dict returned by `_make_juju_status_dict` to YAML.""" + return yaml.dump( + self._make_juju_status_dict( + num_units, service_name, unit_state, machine_state)) + + def test_get_config(self): + # get_config returns the contents of the current charm + # configuration, as returned by config-get --format=json. + mock_config = {'key': 'value'} + + # Monkey-patch shelltoolbox.command to avoid having to call out + # to config-get. + self._patch_command(lambda: dumps(mock_config)) + self.assertEqual(mock_config, charmhelpers.get_config()) + + def test_relation_get(self): + # relation_get returns the value of a given relation variable, + # as returned by relation-get $VAR. + mock_relation_values = { + 'foo': 'bar', + 'spam': 'eggs', + } + self._patch_command(lambda *args: mock_relation_values[args[0]]) + self.assertEqual('bar', charmhelpers.relation_get('foo')) + self.assertEqual('eggs', charmhelpers.relation_get('spam')) + + def test_relation_set(self): + # relation_set calls out to relation-set and passes key=value + # pairs to it. + items_set = {} + def mock_relation_set(*args): + for arg in args: + key, value = arg.split("=") + items_set[key] = value + self._patch_command(mock_relation_set) + charmhelpers.relation_set(foo='bar', spam='eggs') + self.assertEqual('bar', items_set.get('foo')) + self.assertEqual('eggs', items_set.get('spam')) + + def test_make_charm_config_file(self): + # make_charm_config_file() writes the passed configuration to a + # temporary file as YAML. + charm_config = { + 'foo': 'bar', + 'spam': 'eggs', + 'ham': 'jam', + } + # make_charm_config_file() returns the file object so that it + # can be garbage collected properly. + charm_config_file = charmhelpers.make_charm_config_file(charm_config) + with open(charm_config_file.name) as config_in: + written_config = config_in.read() + self.assertEqual(yaml.dump(charm_config), written_config) + + def test_unit_info(self): + # unit_info returns requested data about a given service. + juju_yaml = self._make_juju_status_yaml() + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertEqual( + 'pending', + charmhelpers.unit_info('test-service', 'agent-state')) + + def test_unit_info_returns_empty_for_nonexistent_service(self): + # If the service passed to unit_info() has not yet started (or + # otherwise doesn't exist), unit_info() will return an empty + # string. + juju_yaml = "services: {}" + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertEqual( + '', charmhelpers.unit_info('test-service', 'state')) + + def test_unit_info_accepts_data(self): + # It's possible to pass a `data` dict, containing the parsed + # result of juju status, to unit_info(). + juju_status_data = yaml.safe_load( + self._make_juju_status_yaml()) + self.patch(charmhelpers, 'juju_status', lambda: None) + service_data = juju_status_data['services']['test-service'] + unit_info_dict = service_data['units']['test-service/0'] + for key, value in unit_info_dict.items(): + item_info = charmhelpers.unit_info( + 'test-service', key, data=juju_status_data) + self.assertEqual(value, item_info) + + def test_unit_info_returns_first_unit_by_default(self): + # By default, unit_info() just returns the value of the + # requested item for the first unit in a service. + juju_yaml = self._make_juju_status_yaml(num_units=2) + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + unit_address = charmhelpers.unit_info( + 'test-service', 'public-address') + self.assertEqual('test-service-0.example.com', unit_address) + + def test_unit_info_accepts_unit_name(self): + # By default, unit_info() just returns the value of the + # requested item for the first unit in a service. However, it's + # possible to pass a unit name to it, too. + juju_yaml = self._make_juju_status_yaml(num_units=2) + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + unit_address = charmhelpers.unit_info( + 'test-service', 'public-address', unit='test-service/1') + self.assertEqual('test-service-1.example.com', unit_address) + + def test_get_machine_data(self): + # get_machine_data() returns a dict containing the machine data + # parsed from juju status. + juju_yaml = self._make_juju_status_yaml() + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + machine_0_data = charmhelpers.get_machine_data()[0] + self.assertEqual('zookeeper.example.com', machine_0_data['dns-name']) + + def test_wait_for_machine_returns_if_machine_up(self): + # If wait_for_machine() is called and the machine(s) it is + # waiting for are already up, it will return. + juju_yaml = self._make_juju_status_yaml(machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + machines, time_taken = charmhelpers.wait_for_machine(timeout=1) + self.assertEqual(1, machines) + + def test_wait_for_machine_times_out(self): + # If the machine that wait_for_machine is waiting for isn't + # 'running' before the passed timeout is reached, + # wait_for_machine will raise an error. + juju_yaml = self._make_juju_status_yaml() + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_machine, timeout=0) + + def test_wait_for_machine_always_returns_if_running_locally(self): + # If juju is actually running against a local LXC container, + # wait_for_machine will always return. + juju_status_dict = self._make_juju_status_dict() + # We'll update the 0th machine to make it look like it's an LXC + # container. + juju_status_dict['machines'][0]['dns-name'] = 'localhost' + juju_yaml = yaml.dump(juju_status_dict) + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + machines, time_taken = charmhelpers.wait_for_machine(timeout=1) + # wait_for_machine will always return 1 machine started here, + # since there's only one machine to start. + self.assertEqual(1, machines) + # time_taken will be 0, since no actual waiting happened. + self.assertEqual(0, time_taken) + + def test_wait_for_machine_waits_for_multiple_machines(self): + # wait_for_machine can be told to wait for multiple machines. + juju_yaml = self._make_juju_status_yaml( + num_units=2, machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + machines, time_taken = charmhelpers.wait_for_machine(num_machines=2) + self.assertEqual(2, machines) + + def test_wait_for_unit_returns_if_unit_started(self): + # wait_for_unit() will return if the service it's waiting for is + # already up. + juju_yaml = self._make_juju_status_yaml( + unit_state='started', machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + charmhelpers.wait_for_unit('test-service', timeout=0) + + def test_wait_for_unit_raises_error_on_error_state(self): + # If the unit is in some kind of error state, wait_for_unit will + # raise a RuntimeError. + juju_yaml = self._make_juju_status_yaml( + unit_state='start-error', machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_unit, 'test-service', timeout=0) + + def test_wait_for_unit_raises_error_on_timeout(self): + # If the unit does not start before the timeout is reached, + # wait_for_unit will raise a RuntimeError. + juju_yaml = self._make_juju_status_yaml( + unit_state='pending', machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_unit, 'test-service', timeout=0) + + def test_wait_for_relation_returns_if_relation_up(self): + # wait_for_relation() waits for relations to come up. If a + # relation is already 'up', wait_for_relation() will return + # immediately. + juju_yaml = self._make_juju_status_yaml( + unit_state='started', machine_state='running') + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + charmhelpers.wait_for_relation('test-service', 'db', timeout=0) + + def test_wait_for_relation_times_out_if_relation_not_present(self): + # If a relation does not exist at all before a timeout is + # reached, wait_for_relation() will raise a RuntimeError. + juju_dict = self._make_juju_status_dict( + unit_state='started', machine_state='running') + units = juju_dict['services']['test-service']['units'] + # We'll remove all the relations for test-service for this test. + units['test-service/0']['relations'] = {} + juju_dict['services']['test-service']['units'] = units + juju_yaml = yaml.dump(juju_dict) + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_relation, 'test-service', + 'db', timeout=0) + + def test_wait_for_relation_times_out_if_relation_not_up(self): + # If a relation does not transition to an 'up' state, before a + # timeout is reached, wait_for_relation() will raise a + # RuntimeError. + juju_dict = self._make_juju_status_dict( + unit_state='started', machine_state='running') + units = juju_dict['services']['test-service']['units'] + units['test-service/0']['relations']['db']['state'] = 'down' + juju_dict['services']['test-service']['units'] = units + juju_yaml = yaml.dump(juju_dict) + mock_juju_status = lambda: juju_yaml + self.patch(charmhelpers, 'juju_status', mock_juju_status) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_relation, 'test-service', + 'db', timeout=0) + + def test_wait_for_page_contents_returns_if_contents_available(self): + # wait_for_page_contents() will wait until a given string is + # contained within the results of a given url and will return + # once it does. + # We need to patch the charmhelpers instance of urllib2 so that + # it doesn't try to connect out. + test_content = "Hello, world." + new_urlopen = lambda *args: StringIO(test_content) + self.patch(charmhelpers.urllib2, 'urlopen', new_urlopen) + charmhelpers.wait_for_page_contents( + 'http://example.com', test_content, timeout=0) + + def test_wait_for_page_contents_times_out(self): + # If the desired contents do not appear within the page before + # the specified timeout, wait_for_page_contents() will raise a + # RuntimeError. + # We need to patch the charmhelpers instance of urllib2 so that + # it doesn't try to connect out. + new_urlopen = lambda *args: StringIO("This won't work.") + self.patch(charmhelpers.urllib2, 'urlopen', new_urlopen) + self.assertRaises( + RuntimeError, charmhelpers.wait_for_page_contents, + 'http://example.com', "This will error", timeout=0) + + +if __name__ == '__main__': + unittest.main() === added file 'setup.py' --- setup.py 1970-01-01 00:00:00 +0000 +++ setup.py 2012-04-11 13:26:47 +0000 @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# +# Copyright 2012 Canonical Ltd. This software is licensed under the +# GNU General Public License version 3 (see the file LICENSE). + +import ez_setup + + +ez_setup.use_setuptools() + +from setuptools import setup, find_packages + +__version__ = '0.0.3' + + +setup( + name='charmhelpers', + version=__version__, + packages=find_packages('helpers/python'), + package_dir={'': 'helpers/python'}, + include_package_data=True, + zip_safe=False, + maintainer='Launchpad Yellow', + description=('Helper functions for writing Juju charms'), + license='GPL v3', + url='https://launchpad.net/charm-tools', + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python", + ], +)
-- Mailing list: https://launchpad.net/~yellow Post to : [email protected] Unsubscribe : https://launchpad.net/~yellow More help : https://help.launchpad.net/ListHelp

