Merge lp:~frankban/charms/trusty/redis/initial-charm into lp:~juju-gui/charms/trusty/redis/trunk
- Trusty Tahr (14.04)
- initial-charm
- Merge into trunk
Proposed by
Francesco Banconi
on 2015-05-21
| Status: | Merged |
|---|---|
| Approved by: | Francesco Banconi on 2015-05-25 |
| Approved revision: | 1 |
| Merge reported by: | Francesco Banconi |
| Merged at revision: | not available |
| Proposed branch: | lp:~frankban/charms/trusty/redis/initial-charm |
| Merge into: | lp:~juju-gui/charms/trusty/redis/trunk |
| Diff against target: |
2166 lines (+2007/-0) 25 files modified
.bzrignore (+6/-0) .lbox (+1/-0) .lbox.check (+3/-0) Makefile (+100/-0) README.md (+82/-0) config.yaml (+27/-0) copyright (+18/-0) hooks/configfile.py (+82/-0) hooks/generic-hook (+7/-0) hooks/hookutils.py (+24/-0) hooks/install (+31/-0) hooks/relations.py (+42/-0) hooks/services.py (+98/-0) hooks/serviceutils.py (+109/-0) hooks/setup.py (+19/-0) icon.svg (+468/-0) metadata.yaml (+21/-0) test-requirements.pip (+14/-0) tests/test_10_deploy.py (+313/-0) tests/tests.yaml (+10/-0) unit_tests/test_configfile.py (+90/-0) unit_tests/test_hookutils.py (+58/-0) unit_tests/test_relations.py (+54/-0) unit_tests/test_services.py (+28/-0) unit_tests/test_serviceutils.py (+302/-0) |
| To merge this branch: | bzr merge lp:~frankban/charms/trusty/redis/initial-charm |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Martin Hilton | 2015-05-21 | Approve on 2015-05-22 | |
|
Review via email:
|
|||
Commit Message
Initial implementation of the redis charm.
Description of the Change
Initial implementation of the redis charm.
The charm uses the services framework to implement
the hooks. The readme describes how to deploy
the development version and how to run the tests.
My apologies for the big branch, but this is the
initial commit and most of the code is docs and
tests.
To post a comment you must log in.
| Francesco Banconi (frankban) wrote : | # |
| Martin Hilton (martin-hilton) wrote : | # |
LGTM, with a couple of concerns I'd like you to be satisfied with.
review:
Approve
| Francesco Banconi (frankban) wrote : | # |
Thanks for the review Martin!
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | === added file '.bzrignore' |
| 2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
| 3 | +++ .bzrignore 2015-05-21 17:29:23 +0000 |
| 4 | @@ -0,0 +1,6 @@ |
| 5 | +.coverage |
| 6 | +.emacs* |
| 7 | +.venv |
| 8 | +precise/ |
| 9 | +tags |
| 10 | +trusty/ |
| 11 | |
| 12 | === added file '.lbox' |
| 13 | --- .lbox 1970-01-01 00:00:00 +0000 |
| 14 | +++ .lbox 2015-05-21 17:29:23 +0000 |
| 15 | @@ -0,0 +1,1 @@ |
| 16 | +propose -cr -for lp:~juju-gui/charms/trusty/redis/trunk |
| 17 | |
| 18 | === added file '.lbox.check' |
| 19 | --- .lbox.check 1970-01-01 00:00:00 +0000 |
| 20 | +++ .lbox.check 2015-05-21 17:29:23 +0000 |
| 21 | @@ -0,0 +1,3 @@ |
| 22 | +#!/bin/sh |
| 23 | + |
| 24 | +make lint unittest |
| 25 | |
| 26 | === added file 'Makefile' |
| 27 | --- Makefile 1970-01-01 00:00:00 +0000 |
| 28 | +++ Makefile 2015-05-21 17:29:23 +0000 |
| 29 | @@ -0,0 +1,100 @@ |
| 30 | +# Copyright 2015 Canonical Ltd. |
| 31 | +# Licensed under the GPLv3, see copyright file for details. |
| 32 | + |
| 33 | +export JUJU_TEST_CHARM=redis |
| 34 | +export JUJU_REPOSITORY= |
| 35 | + |
| 36 | +# Override those values to run functional tests with a different environment |
| 37 | +# or Juju series. |
| 38 | +SERIES=trusty |
| 39 | +JUJU_ENV=local |
| 40 | + |
| 41 | +# Define system Debian dependencies: run "make sysdeps" to install them. |
| 42 | +# Please keep them in alphabetical order. |
| 43 | +SYSDEPS = charm-tools juju-core juju-local python-dev python-pip \ |
| 44 | + python-virtualenv rsync |
| 45 | + |
| 46 | +PYTHON = python |
| 47 | +VENV = .venv |
| 48 | +VENV_ACTIVATE = $(VENV)/bin/activate |
| 49 | +NOSE = $(VENV)/bin/nosetests |
| 50 | +PIP = $(VENV)/bin/pip |
| 51 | +FTESTS=$(shell find -L tests -type f -executable | sort) |
| 52 | + |
| 53 | +.DEFAULT_GOAL := setup |
| 54 | + |
| 55 | +.PHONY: help |
| 56 | +help: |
| 57 | + @echo -e 'Redis charm - list of make targets:\n' |
| 58 | + @echo 'make - Set up the development and testing environment.' |
| 59 | + @echo 'make test - Run tests (including unit and functional tests).' |
| 60 | + @echo 'make lint - Run linter and pep8.' |
| 61 | + @echo 'make check - Run all the tests and lint.' |
| 62 | + @echo 'make unittest - Run unit tests.' |
| 63 | + @echo 'make ftest - Run functional tests.' |
| 64 | + @echo 'make clean - Get rid of bytecode files and virtual envs.' |
| 65 | + @echo 'make deploy - Deploy the local copy of the charm.' |
| 66 | + @echo -e '\nWhen using "make deploy" it is possible to override the series' |
| 67 | + @echo 'with the SERIES environment variable and the service name with ' |
| 68 | + @echo 'the JUJU_SERVICE_NAME environment variable, for instance:' |
| 69 | + @echo ' make deploy SERIES=precise JUJU_SERVICE_NAME=myredis' |
| 70 | + |
| 71 | +.PHONY: setup |
| 72 | +setup: $(VENV_ACTIVATE) |
| 73 | + |
| 74 | +.PHONY: sysdeps |
| 75 | +sysdeps: |
| 76 | + sudo apt-get install --yes $(SYSDEPS) |
| 77 | + |
| 78 | +$(VENV_ACTIVATE): test-requirements.pip |
| 79 | + virtualenv --distribute -p $(PYTHON) $(VENV) |
| 80 | + $(PIP) install -r test-requirements.pip || \ |
| 81 | + (touch test-requirements.pip; exit 1) |
| 82 | + @touch $(VENV_ACTIVATE) |
| 83 | + |
| 84 | +.PHONY: clean |
| 85 | +clean: |
| 86 | + -$(RM) -rfv $(VENV) .coverage |
| 87 | + find . -name '*.pyc' -delete |
| 88 | + |
| 89 | +.PHONY: check |
| 90 | +check: lint test |
| 91 | + juju charm proof |
| 92 | + |
| 93 | +.PHONY: lint |
| 94 | +lint: setup |
| 95 | + @$(VENV)/bin/flake8 --show-source --exclude=$(VENV) \ |
| 96 | + --filename *.py,install,generic-hook \ |
| 97 | + hooks/ tests/ unit_tests/ |
| 98 | + |
| 99 | +.PHONY: deploy |
| 100 | +deploy: |
| 101 | + @# The use of readlink below is required for OS X. |
| 102 | + @$(eval export JUJU_REPOSITORY:=$(shell mktemp -d `readlink -f /tmp`/temp.XXXX)) |
| 103 | + @echo "JUJU_REPOSITORY is $(JUJU_REPOSITORY)" |
| 104 | + @# Setting up the Juju repository. |
| 105 | + @mkdir $(JUJU_REPOSITORY)/${SERIES} |
| 106 | + @rsync -a . $(JUJU_REPOSITORY)/${SERIES}/$(JUJU_TEST_CHARM) \ |
| 107 | + --exclude .git --exclude .bzr --exclude tests --exclude unit_tests |
| 108 | + @# Deploying the charm. |
| 109 | + juju deploy local:${SERIES}/$(JUJU_TEST_CHARM) $(JUJU_SERVICE_NAME) |
| 110 | + |
| 111 | +.PHONY: test |
| 112 | +test: unittest ftest |
| 113 | + |
| 114 | +.PHONY: ftest |
| 115 | +ftest: |
| 116 | + juju bootstrap -e $(JUJU_ENV) --upload-tools |
| 117 | + @ # Wait for the environment to be ready. |
| 118 | + juju status -e $(JUJU_ENV) |
| 119 | + @# Setting the path is required because internally amulet calls |
| 120 | + @# juju-deployer using subprocess. |
| 121 | + PATH="$(VENV)/bin:$(PATH)" $(NOSE) --verbosity 2 -s $(FTESTS) |
| 122 | + juju destroy-environment $(JUJU_ENV) -y |
| 123 | + @# Clean up deployer garbage. |
| 124 | + @-$(RM) -rf trusty |
| 125 | + |
| 126 | +.PHONY: unittest |
| 127 | +unittest: setup |
| 128 | + $(NOSE) --verbosity 2 -s -w unit_tests \ |
| 129 | + --with-coverage --cover-package hooks --cover-erase |
| 130 | |
| 131 | === added file 'README.md' |
| 132 | --- README.md 1970-01-01 00:00:00 +0000 |
| 133 | +++ README.md 2015-05-21 17:29:23 +0000 |
| 134 | @@ -0,0 +1,82 @@ |
| 135 | +# Overview |
| 136 | + |
| 137 | +Redis (<http://redis.io>) is an open source, advanced key-value cache and |
| 138 | +store. It is often referred to as a data structure server since keys can |
| 139 | +contain strings, hashes, lists, sets, sorted sets, bitmaps and hyperloglogs. |
| 140 | +In order to achieve its outstanding performance, Redis works with an in-memory |
| 141 | +dataset that can be written to disk. Redis also supports master-slave |
| 142 | +asynchronous replication. |
| 143 | + |
| 144 | +Redis can be configured in a master or slave configuration. This charm |
| 145 | +provides a single stand alone implementation of Redis software supporting the |
| 146 | +master-slave relationship. Go to the Redis web pages for more information on |
| 147 | +[replication](http://redis.io/topics/replication). |
| 148 | + |
| 149 | +# Usage |
| 150 | + |
| 151 | +To deploy this charm first bootstrap your Juju environment and issue the |
| 152 | +following command: |
| 153 | + |
| 154 | + juju deploy redis |
| 155 | + |
| 156 | +Expose the master if you need to contact them for some reason. |
| 157 | + |
| 158 | + juju expose redis |
| 159 | + |
| 160 | +# Replication |
| 161 | + |
| 162 | +Redis can be set up with master-slave replication in Juju. This allows the |
| 163 | +Redis slave to be an exact copy of master server. A master can have multiple |
| 164 | +slaves. |
| 165 | + |
| 166 | +To set up a master-slave scenario, deploy two services using this charm, one |
| 167 | +for the master and one the slave server, then relate the two. |
| 168 | + |
| 169 | + juju deploy redis redis1 |
| 170 | + juju deploy redis redis2 |
| 171 | + juju add-relation redis1:master redis2:slave |
| 172 | + |
| 173 | +# Connecting to the charm |
| 174 | + |
| 175 | +The charm provides a `db` relation for services wanting to connect to the Redis |
| 176 | +service. When the relation established the following data is passed to the |
| 177 | +related units: |
| 178 | + |
| 179 | +- `hostname`: the Redis server host name; |
| 180 | +- `port`: the port the Redis server is listening to; |
| 181 | +- `password`: the optional authentication password, or an empty string if no |
| 182 | + authentication is required. |
| 183 | + |
| 184 | +# Testing Redis |
| 185 | + |
| 186 | +To test if Redis software is functioning properly telnet to the redis ip |
| 187 | +address using port 6379: |
| 188 | + |
| 189 | + telnet <redis-ip> 6379 |
| 190 | + |
| 191 | +You can also install the redis-tools package `apt-get install redis-tools` |
| 192 | +and connect using the Redis client command: |
| 193 | + |
| 194 | + redis-cli |
| 195 | + |
| 196 | +From there you can issue [Redis commands](http://redis.io/commands) to test |
| 197 | +that Redis is working as intended. |
| 198 | + |
| 199 | +# Development and automated testing. |
| 200 | + |
| 201 | +To create a development environment, obtain a copy of the sources, run |
| 202 | +`make sysdeps` to install the required system packages and then `make` to set |
| 203 | +up the development virtual environment. At this point, it is possible to run |
| 204 | +unit and functional tests, including lint checks, by executing `make check`. |
| 205 | + |
| 206 | +Run `make deploy` to deploy the local copy of the charm for development |
| 207 | +purposes on your already bootstrapped environment. |
| 208 | + |
| 209 | +Use `make help` for further information about available make targets. |
| 210 | + |
| 211 | +# Redis Information |
| 212 | + |
| 213 | +- Redis [home page](http://redis.io/) |
| 214 | +- Redis [github bug tracker](https://github.com/antirez/redis/issues) |
| 215 | +- Redis [documentation](http://redis.io/documentation) |
| 216 | +- Redis [mailing list](http://groups.google.com/group/redis-db) |
| 217 | |
| 218 | === added file 'config.yaml' |
| 219 | --- config.yaml 1970-01-01 00:00:00 +0000 |
| 220 | +++ config.yaml 2015-05-21 17:29:23 +0000 |
| 221 | @@ -0,0 +1,27 @@ |
| 222 | +options: |
| 223 | + port: |
| 224 | + type: int |
| 225 | + default: 6379 |
| 226 | + description: | |
| 227 | + Accept connections on the specified port. |
| 228 | + password: |
| 229 | + type: string |
| 230 | + default: "" |
| 231 | + description: | |
| 232 | + Require clients to issue AUTH <PASSWORD> before processing any other |
| 233 | + commands. |
| 234 | + loglevel: |
| 235 | + type: string |
| 236 | + default: notice |
| 237 | + description: | |
| 238 | + Specify the Redis server verbosity level. Choices are: |
| 239 | + - debug (a lot of information, useful for development/testing); |
| 240 | + - verbose (many rarely useful info, but not a mess like the debug level); |
| 241 | + - notice (moderately verbose, what you want in production probably); |
| 242 | + - warning (only very important / critical messages are logged). |
| 243 | + logfile: |
| 244 | + type: string |
| 245 | + default: /var/log/redis/redis-server.log |
| 246 | + description: | |
| 247 | + Spcify the log file name. |
| 248 | + |
| 249 | |
| 250 | === added file 'copyright' |
| 251 | --- copyright 1970-01-01 00:00:00 +0000 |
| 252 | +++ copyright 2015-05-21 17:29:23 +0000 |
| 253 | @@ -0,0 +1,18 @@ |
| 254 | +Format: http://dep.debian.net/deps/dep5/ |
| 255 | + |
| 256 | +Files: * |
| 257 | +Copyright: Copyright 2015, Canonical Ltd., All Rights Reserved. |
| 258 | +License: GPL-3 |
| 259 | + This program is free software: you can redistribute it and/or modify |
| 260 | + it under the terms of the GNU General Public License as published by |
| 261 | + the Free Software Foundation, either version 3 of the License, or |
| 262 | + (at your option) any later version. |
| 263 | + . |
| 264 | + This program is distributed in the hope that it will be useful, |
| 265 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 266 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 267 | + GNU General Public License for more details. |
| 268 | + . |
| 269 | + You should have received a copy of the GNU General Public License |
| 270 | + along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 271 | + |
| 272 | |
| 273 | === added directory 'hooks' |
| 274 | === added symlink 'hooks/config-changed' |
| 275 | === target is u'generic-hook' |
| 276 | === added file 'hooks/configfile.py' |
| 277 | --- hooks/configfile.py 1970-01-01 00:00:00 +0000 |
| 278 | +++ hooks/configfile.py 2015-05-21 17:29:23 +0000 |
| 279 | @@ -0,0 +1,82 @@ |
| 280 | +# Copyright 2015 Canonical Ltd. |
| 281 | +# Licensed under the GPLv3, see copyright file for details. |
| 282 | + |
| 283 | +"""Utilities for working with the redis configuration file.""" |
| 284 | + |
| 285 | +import errno |
| 286 | +import os |
| 287 | +import shutil |
| 288 | +import tempfile |
| 289 | + |
| 290 | + |
| 291 | +# Define the paths to the redis default and customized configuration file. |
| 292 | +DEFAULT_REDIS_CONF = '/etc/redis/redis.conf' |
| 293 | +REDIS_CONF = '/etc/redis/redis-charm.conf' |
| 294 | + |
| 295 | + |
| 296 | +def include_config(target): |
| 297 | + """Include target configuration file at the end of the default config. |
| 298 | + |
| 299 | + Note that this function is not idempotent. For this reason, it is only safe |
| 300 | + to call it in the install hook. |
| 301 | + |
| 302 | + Raise an IOError if the configuration file does not exist or it is not |
| 303 | + writable. |
| 304 | + """ |
| 305 | + _backup(DEFAULT_REDIS_CONF) |
| 306 | + with open(DEFAULT_REDIS_CONF, 'a') as conf_file: |
| 307 | + conf_file.write('include {}\n'.format(target)) |
| 308 | + |
| 309 | + |
| 310 | +def write(options, target): |
| 311 | + """Write the redis customized configuration file. |
| 312 | + |
| 313 | + Receive the redis charm configuration options and the target file where to |
| 314 | + write configuration to. |
| 315 | + |
| 316 | + Report whether the new and old configurations differ. |
| 317 | + Raise an IOError if a problem is encountered in the operation. |
| 318 | + """ |
| 319 | + try: |
| 320 | + old_content = open(target, 'r').read() |
| 321 | + except IOError as err: |
| 322 | + if err.errno != errno.ENOENT: |
| 323 | + raise |
| 324 | + # It is acceptable that the customized configuration does not exist. |
| 325 | + old_content = '' |
| 326 | + new_content = ''.join( |
| 327 | + '{} {}\n'.format(key, value) for key, value in sorted(options.items())) |
| 328 | + # If there are no differences in the new and old configuration, we can |
| 329 | + # avoid writing the file and/or doing backups. |
| 330 | + if new_content == old_content: |
| 331 | + return False |
| 332 | + # The backup is only done if the target file has content. |
| 333 | + if old_content: |
| 334 | + _backup(target) |
| 335 | + # Write the new configuration in a new file, then rename to the real file. |
| 336 | + # Since the renaming operation may fail on some Unix flavors if the source |
| 337 | + # and destination files are on different file systems, use for the |
| 338 | + # temporary file the same directory where the target is stored. |
| 339 | + dirname = os.path.dirname(target) |
| 340 | + temp_file = tempfile.NamedTemporaryFile( |
| 341 | + prefix='charm-new-config-', dir=dirname, delete=False) |
| 342 | + temp_file.write(new_content) |
| 343 | + # Ensure that all the data is written to disk. |
| 344 | + temp_file.flush() |
| 345 | + os.fsync(temp_file.fileno()) |
| 346 | + temp_file.close() |
| 347 | + os.chmod(temp_file.name, 0644) |
| 348 | + # Rename the temporary file to the real target file. |
| 349 | + os.rename(temp_file.name, target) |
| 350 | + return True |
| 351 | + |
| 352 | + |
| 353 | +def _backup(filename): |
| 354 | + """Create a backup copy of the given file. |
| 355 | + |
| 356 | + Return the path to the backup copy. |
| 357 | + Raise an IOError if a problem occurs while copying the file. |
| 358 | + """ |
| 359 | + backup_filename = filename + '.bak' |
| 360 | + shutil.copyfile(filename, backup_filename) |
| 361 | + return backup_filename |
| 362 | |
| 363 | === added symlink 'hooks/db-relation-broken' |
| 364 | === target is u'generic-hook' |
| 365 | === added symlink 'hooks/db-relation-changed' |
| 366 | === target is u'generic-hook' |
| 367 | === added symlink 'hooks/db-relation-departed' |
| 368 | === target is u'generic-hook' |
| 369 | === added symlink 'hooks/db-relation-joined' |
| 370 | === target is u'generic-hook' |
| 371 | === added file 'hooks/generic-hook' |
| 372 | --- hooks/generic-hook 1970-01-01 00:00:00 +0000 |
| 373 | +++ hooks/generic-hook 2015-05-21 17:29:23 +0000 |
| 374 | @@ -0,0 +1,7 @@ |
| 375 | +#!/usr/bin/python |
| 376 | + |
| 377 | +# Copyright 2015 Canonical Ltd. |
| 378 | +# Licensed under the GPLv3, see copyright file for details. |
| 379 | + |
| 380 | +import services |
| 381 | +services.manage() |
| 382 | |
| 383 | === added file 'hooks/hookutils.py' |
| 384 | --- hooks/hookutils.py 1970-01-01 00:00:00 +0000 |
| 385 | +++ hooks/hookutils.py 2015-05-21 17:29:23 +0000 |
| 386 | @@ -0,0 +1,24 @@ |
| 387 | +# Copyright 2015 Canonical Ltd. |
| 388 | +# Licensed under the GPLv3, see copyright file for details. |
| 389 | + |
| 390 | +"""Helper functions for handling hooks execution.""" |
| 391 | + |
| 392 | +import functools |
| 393 | + |
| 394 | +from charmhelpers.core import hookenv |
| 395 | + |
| 396 | + |
| 397 | +def hook_name_logged(function): |
| 398 | + """Decorate the given function so that the current hook name is logged. |
| 399 | + |
| 400 | + The given function must accept no arguments. |
| 401 | + """ |
| 402 | + @functools.wraps(function) |
| 403 | + def decorated(): |
| 404 | + hook_name = hookenv.hook_name() |
| 405 | + hookenv.log('>>> Entering hook: {}.'.format(hook_name)) |
| 406 | + try: |
| 407 | + return function() |
| 408 | + finally: |
| 409 | + hookenv.log('<<< Exiting hook: {}.'.format(hook_name)) |
| 410 | + return decorated |
| 411 | |
| 412 | === added file 'hooks/install' |
| 413 | --- hooks/install 1970-01-01 00:00:00 +0000 |
| 414 | +++ hooks/install 2015-05-21 17:29:23 +0000 |
| 415 | @@ -0,0 +1,31 @@ |
| 416 | +#!/usr/bin/python |
| 417 | + |
| 418 | +# Copyright 2015 Canonical Ltd. |
| 419 | +# Licensed under the GPLv3, see copyright file for details. |
| 420 | + |
| 421 | +import setup |
| 422 | +setup.pre_install() |
| 423 | + |
| 424 | +from charmhelpers import fetch |
| 425 | +from charmhelpers.core import hookenv |
| 426 | + |
| 427 | +import configfile |
| 428 | +import hookutils |
| 429 | + |
| 430 | + |
| 431 | +# Define Debian packages to be installed. |
| 432 | +PACKAGES = ['redis-server'] |
| 433 | + |
| 434 | + |
| 435 | +@hookutils.hook_name_logged |
| 436 | +def install(): |
| 437 | + """Install the Debian packages required by redis.""" |
| 438 | + hookenv.log('Installing system packages.') |
| 439 | + fetch.apt_install(fetch.filter_installed_packages(PACKAGES)) |
| 440 | + # Include the customized configuration file at the end of the default |
| 441 | + # redis configuration file. |
| 442 | + configfile.include_config(configfile.REDIS_CONF) |
| 443 | + |
| 444 | + |
| 445 | +if __name__ == "__main__": |
| 446 | + install() |
| 447 | |
| 448 | === added symlink 'hooks/master-relation-broken' |
| 449 | === target is u'generic-hook' |
| 450 | === added symlink 'hooks/master-relation-changed' |
| 451 | === target is u'generic-hook' |
| 452 | === added symlink 'hooks/master-relation-departed' |
| 453 | === target is u'generic-hook' |
| 454 | === added symlink 'hooks/master-relation-joined' |
| 455 | === target is u'generic-hook' |
| 456 | === added file 'hooks/relations.py' |
| 457 | --- hooks/relations.py 1970-01-01 00:00:00 +0000 |
| 458 | +++ hooks/relations.py 2015-05-21 17:29:23 +0000 |
| 459 | @@ -0,0 +1,42 @@ |
| 460 | +# Copyright 2015 Canonical Ltd. |
| 461 | +# Licensed under the GPLv3, see copyright file for details. |
| 462 | + |
| 463 | +"""Define service relations for the redis charm.""" |
| 464 | + |
| 465 | +from charmhelpers.core import hookenv |
| 466 | +from charmhelpers.core.services import helpers |
| 467 | + |
| 468 | + |
| 469 | +class DbRelation(helpers.RelationContext): |
| 470 | + """Define the redis db relation. |
| 471 | + |
| 472 | + Subscribers are provided the server "hostname", "port" and "password" |
| 473 | + values in the relation payload. If the redis server does not use |
| 474 | + authentication, the password is an empty string. |
| 475 | + """ |
| 476 | + |
| 477 | + name = 'db' |
| 478 | + interface = 'redis' |
| 479 | + |
| 480 | + def provide_data(self): |
| 481 | + """Return data to be relation_set for this interface.""" |
| 482 | + config = hookenv.config() |
| 483 | + return { |
| 484 | + 'hostname': hookenv.unit_get('public-address'), |
| 485 | + 'port': config['port'], |
| 486 | + 'password': config['password'].strip(), |
| 487 | + } |
| 488 | + |
| 489 | + |
| 490 | +class MasterRelation(DbRelation): |
| 491 | + """Define the redis master relation.""" |
| 492 | + |
| 493 | + name = 'master' |
| 494 | + |
| 495 | + |
| 496 | +class SlaveRelation(helpers.RelationContext): |
| 497 | + """Define the redis slave relation.""" |
| 498 | + |
| 499 | + name = 'slave' |
| 500 | + interface = 'redis' |
| 501 | + required_keys = ['hostname', 'port'] |
| 502 | |
| 503 | === added file 'hooks/services.py' |
| 504 | --- hooks/services.py 1970-01-01 00:00:00 +0000 |
| 505 | +++ hooks/services.py 2015-05-21 17:29:23 +0000 |
| 506 | @@ -0,0 +1,98 @@ |
| 507 | +#!/usr/bin/python |
| 508 | + |
| 509 | +# Copyright 2015 Canonical Ltd. |
| 510 | +# Licensed under the GPLv3, see copyright file for details. |
| 511 | + |
| 512 | +"""Redis charm service definitions and management. |
| 513 | + |
| 514 | +This charm uses the service framework to handle all of its hooks except for the |
| 515 | +install hook. See https://pythonhosted.org/charmhelpers/examples/services.html |
| 516 | + |
| 517 | +Two service definitions are provided to the manager: redis-master and |
| 518 | +redis-slave. The idea is that either the former or the latter can be ready at |
| 519 | +the same time, but not both or none. If the "redis1:master redis2:slave" |
| 520 | +relation is established and ready, then the redis-slave service definition is |
| 521 | +enabled on redis2 units. In all the other cases the redis-master definition is |
| 522 | +enabled. |
| 523 | + |
| 524 | +The redis server itself is always running, and it is only restarted when a |
| 525 | +change is detected in its configuration file, due to charm config changes or to |
| 526 | +slave relation established. |
| 527 | +""" |
| 528 | + |
| 529 | +from charmhelpers.core import hookenv |
| 530 | +from charmhelpers.core.services import base |
| 531 | + |
| 532 | +import hookutils |
| 533 | +import serviceutils |
| 534 | +import relations |
| 535 | + |
| 536 | + |
| 537 | +@hookutils.hook_name_logged |
| 538 | +def manage(): |
| 539 | + """Set up the service manager for redis.""" |
| 540 | + config = hookenv.config() |
| 541 | + # Handle relations. |
| 542 | + db_relation = relations.DbRelation() |
| 543 | + master_relation = relations.MasterRelation() |
| 544 | + slave_relation = relations.SlaveRelation() |
| 545 | + slave_relation_ready = slave_relation.is_ready() |
| 546 | + |
| 547 | + # Set up the service manager. |
| 548 | + manager = base.ServiceManager([ |
| 549 | + { |
| 550 | + # The name of the redis master service. |
| 551 | + 'service': 'redis-master', |
| 552 | + |
| 553 | + # Ports to open when the service starts. |
| 554 | + 'ports': [config['port']], |
| 555 | + |
| 556 | + # Context managers for provided relations. |
| 557 | + 'provided_data': [db_relation, master_relation], |
| 558 | + |
| 559 | + # Data (contexts) required to start the service. |
| 560 | + 'required_data': [config, not slave_relation_ready], |
| 561 | + |
| 562 | + # Callables called when required data is ready. |
| 563 | + 'data_ready': [ |
| 564 | + serviceutils.write_config_file( |
| 565 | + config, |
| 566 | + db_relation=db_relation, |
| 567 | + master_relation=master_relation), |
| 568 | + ], |
| 569 | + |
| 570 | + # Callables called when it is time to start the service. |
| 571 | + 'start': [serviceutils.service_start], |
| 572 | + |
| 573 | + # Callables called when it is time to stop the service. |
| 574 | + 'stop': [serviceutils.service_stop], |
| 575 | + }, |
| 576 | + { |
| 577 | + # The name of the redis slave service. |
| 578 | + 'service': 'redis-slave', |
| 579 | + |
| 580 | + # Ports to open when the service starts. |
| 581 | + 'ports': [config['port']], |
| 582 | + |
| 583 | + # Context managers for provided relations. |
| 584 | + 'provided_data': [db_relation], |
| 585 | + |
| 586 | + # Data (contexts) required to start the service. |
| 587 | + 'required_data': [config, slave_relation_ready], |
| 588 | + |
| 589 | + # Callables called when required data is ready. |
| 590 | + 'data_ready': [ |
| 591 | + serviceutils.write_config_file( |
| 592 | + config, |
| 593 | + db_relation=db_relation, |
| 594 | + slave_relation=slave_relation), |
| 595 | + ], |
| 596 | + |
| 597 | + # Callables called when it is time to start the service. |
| 598 | + 'start': [serviceutils.service_start], |
| 599 | + |
| 600 | + # Callables called when it is time to stop the service. |
| 601 | + 'stop': [serviceutils.service_stop], |
| 602 | + } |
| 603 | + ]) |
| 604 | + manager.manage() |
| 605 | |
| 606 | === added file 'hooks/serviceutils.py' |
| 607 | --- hooks/serviceutils.py 1970-01-01 00:00:00 +0000 |
| 608 | +++ hooks/serviceutils.py 2015-05-21 17:29:23 +0000 |
| 609 | @@ -0,0 +1,109 @@ |
| 610 | +# Copyright 2015 Canonical Ltd. |
| 611 | +# Licensed under the GPLv3, see copyright file for details. |
| 612 | + |
| 613 | +"""Service manager helpers. |
| 614 | + |
| 615 | +This module includes closures and callbacks suitable to be used when |
| 616 | +registering callables in the services framework manager. |
| 617 | +""" |
| 618 | + |
| 619 | +from charmhelpers.core import ( |
| 620 | + hookenv, |
| 621 | + host, |
| 622 | +) |
| 623 | + |
| 624 | +import configfile |
| 625 | + |
| 626 | + |
| 627 | +# Define the name of the init service set up when installing redis. |
| 628 | +SERVICE_NAME = 'redis-server' |
| 629 | + |
| 630 | + |
| 631 | +def service_start(service_name): |
| 632 | + """Start the service if not already running.""" |
| 633 | + if not host.service_running(SERVICE_NAME): |
| 634 | + hookenv.log('Starting service {}.'.format(service_name)) |
| 635 | + host.service_start(SERVICE_NAME) |
| 636 | + |
| 637 | + |
| 638 | +def service_stop(service_name): |
| 639 | + """Stop the service if it is running and if the stop hook is executing.""" |
| 640 | + if hookenv.hook_name() == 'stop' and host.service_running(SERVICE_NAME): |
| 641 | + # There is no need to stop the service if we are not in the stop hook. |
| 642 | + hookenv.log('Stopping service {}.'.format(service_name)) |
| 643 | + host.service_stop(SERVICE_NAME) |
| 644 | + # XXX (frankban): remove redis package and clean up files. |
| 645 | + |
| 646 | + |
| 647 | +def write_config_file( |
| 648 | + config, db_relation=None, master_relation=None, slave_relation=None): |
| 649 | + """Wrap the configfile.write function building options for the config. |
| 650 | + |
| 651 | + The config argument is the hook environment configuration. |
| 652 | + The relation arguments are relation context, and when passed they are |
| 653 | + assumed to be ready. |
| 654 | + |
| 655 | + Return a function that can be used as a callback in the services framework, |
| 656 | + and that generates the redis configuration file. |
| 657 | + |
| 658 | + This returned functions also takes care of restarting the service if the |
| 659 | + configuration changed. |
| 660 | + """ |
| 661 | + def callback(service_name): |
| 662 | + options = _get_service_options(config, slave_relation) |
| 663 | + hookenv.log('Writing configuration file for {}.'.format(service_name)) |
| 664 | + changed = configfile.write(options, configfile.REDIS_CONF) |
| 665 | + if changed: |
| 666 | + hookenv.log('Restarting service due to configuration change.') |
| 667 | + host.service_restart(SERVICE_NAME) |
| 668 | + # If the configuration changed, it is possible that related units |
| 669 | + # require notification of changes. For this reason, update all the |
| 670 | + # existing established relations. This is required because |
| 671 | + # "services.provide_data" is only called when the current hook |
| 672 | + # is a relation joined or changed. |
| 673 | + _update_relations(filter(None, [db_relation, master_relation])) |
| 674 | + else: |
| 675 | + hookenv.log('No changes detected in the configuration file.') |
| 676 | + |
| 677 | + return callback |
| 678 | + |
| 679 | + |
| 680 | +def _get_service_options(config, slave_relation=None): |
| 681 | + """Return a dict containing the redis service configuration options. |
| 682 | + |
| 683 | + Receive the hook environment config object and optionally the slave |
| 684 | + relation context. |
| 685 | + """ |
| 686 | + hookenv.log('Retrieving service options.') |
| 687 | + # To introduce more redis configuration options in the charm, add them to |
| 688 | + # the config.yaml file and to the dictionary returned by this function. |
| 689 | + # If the new options are relevant while establishing relations, also update |
| 690 | + # the "provide_data" methods in the relation contexts defined in |
| 691 | + # relations.py. |
| 692 | + options = { |
| 693 | + 'bind': hookenv.unit_get('public-address'), |
| 694 | + 'logfile': config['logfile'], |
| 695 | + 'loglevel': config['loglevel'], |
| 696 | + 'port': config['port'], |
| 697 | + } |
| 698 | + password = config['password'].strip() |
| 699 | + if password: |
| 700 | + options['requirepass'] = password |
| 701 | + if slave_relation is not None: |
| 702 | + hookenv.log('Setting up slave relation.') |
| 703 | + # If slave_relation is defined, it is assumed that the relation is |
| 704 | + # ready, i.e. that the slave_relation dict evaluates to True. |
| 705 | + data = slave_relation[slave_relation.name][0] |
| 706 | + options['slaveof'] = '{hostname} {port}'.format(**data) |
| 707 | + password = data.get('password') |
| 708 | + if password: |
| 709 | + options['masterauth'] = password |
| 710 | + return options |
| 711 | + |
| 712 | + |
| 713 | +def _update_relations(relations): |
| 714 | + """Update existing established relations.""" |
| 715 | + for relation in relations: |
| 716 | + for relation_id in hookenv.relation_ids(relation.name): |
| 717 | + hookenv.log('Updating data for relation {}.'.format(relation.name)) |
| 718 | + hookenv.relation_set(relation_id, relation.provide_data()) |
| 719 | |
| 720 | === added file 'hooks/setup.py' |
| 721 | --- hooks/setup.py 1970-01-01 00:00:00 +0000 |
| 722 | +++ hooks/setup.py 2015-05-21 17:29:23 +0000 |
| 723 | @@ -0,0 +1,19 @@ |
| 724 | +# Copyright 2015 Canonical Ltd. |
| 725 | +# Licensed under the GPLv3, see copyright file for details. |
| 726 | + |
| 727 | +"""Set up charmhelpers on the unit.""" |
| 728 | + |
| 729 | + |
| 730 | +def pre_install(): |
| 731 | + """Do any setup required before the install hook.""" |
| 732 | + install_charmhelpers() |
| 733 | + |
| 734 | + |
| 735 | +def install_charmhelpers(): |
| 736 | + """Install the charmhelpers library, if not present.""" |
| 737 | + try: |
| 738 | + import charmhelpers # noqa |
| 739 | + except ImportError: |
| 740 | + import subprocess |
| 741 | + subprocess.check_call(['apt-get', 'install', '-y', 'python-pip']) |
| 742 | + subprocess.check_call(['pip', 'install', 'charmhelpers==0.3.1']) |
| 743 | |
| 744 | === added symlink 'hooks/slave-relation-broken' |
| 745 | === target is u'generic-hook' |
| 746 | === added symlink 'hooks/slave-relation-changed' |
| 747 | === target is u'generic-hook' |
| 748 | === added symlink 'hooks/slave-relation-departed' |
| 749 | === target is u'generic-hook' |
| 750 | === added symlink 'hooks/slave-relation-joined' |
| 751 | === target is u'generic-hook' |
| 752 | === added symlink 'hooks/start' |
| 753 | === target is u'generic-hook' |
| 754 | === added symlink 'hooks/stop' |
| 755 | === target is u'generic-hook' |
| 756 | === added symlink 'hooks/upgrade-charm' |
| 757 | === target is u'generic-hook' |
| 758 | === added file 'icon.svg' |
| 759 | --- icon.svg 1970-01-01 00:00:00 +0000 |
| 760 | +++ icon.svg 2015-05-21 17:29:23 +0000 |
| 761 | @@ -0,0 +1,468 @@ |
| 762 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
| 763 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> |
| 764 | + |
| 765 | +<svg |
| 766 | + xmlns:dc="http://purl.org/dc/elements/1.1/" |
| 767 | + xmlns:cc="http://creativecommons.org/ns#" |
| 768 | + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
| 769 | + xmlns:svg="http://www.w3.org/2000/svg" |
| 770 | + xmlns="http://www.w3.org/2000/svg" |
| 771 | + xmlns:xlink="http://www.w3.org/1999/xlink" |
| 772 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
| 773 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
| 774 | + width="96" |
| 775 | + height="96" |
| 776 | + id="svg6517" |
| 777 | + version="1.1" |
| 778 | + inkscape:version="0.48+devel r12505" |
| 779 | + sodipodi:docname="redis.svg"> |
| 780 | + <defs |
| 781 | + id="defs6519"> |
| 782 | + <filter |
| 783 | + style="color-interpolation-filters:sRGB;" |
| 784 | + inkscape:label="Inner Shadow" |
| 785 | + id="filter1121"> |
| 786 | + <feFlood |
| 787 | + flood-opacity="0.59999999999999998" |
| 788 | + flood-color="rgb(0,0,0)" |
| 789 | + result="flood" |
| 790 | + id="feFlood1123" /> |
| 791 | + <feComposite |
| 792 | + in="flood" |
| 793 | + in2="SourceGraphic" |
| 794 | + operator="out" |
| 795 | + result="composite1" |
| 796 | + id="feComposite1125" /> |
| 797 | + <feGaussianBlur |
| 798 | + in="composite1" |
| 799 | + stdDeviation="1" |
| 800 | + result="blur" |
| 801 | + id="feGaussianBlur1127" /> |
| 802 | + <feOffset |
| 803 | + dx="0" |
| 804 | + dy="2" |
| 805 | + result="offset" |
| 806 | + id="feOffset1129" /> |
| 807 | + <feComposite |
| 808 | + in="offset" |
| 809 | + in2="SourceGraphic" |
| 810 | + operator="atop" |
| 811 | + result="composite2" |
| 812 | + id="feComposite1131" /> |
| 813 | + </filter> |
| 814 | + <filter |
| 815 | + style="color-interpolation-filters:sRGB;" |
| 816 | + inkscape:label="Drop Shadow" |
| 817 | + id="filter950"> |
| 818 | + <feFlood |
| 819 | + flood-opacity="0.25" |
| 820 | + flood-color="rgb(0,0,0)" |
| 821 | + result="flood" |
| 822 | + id="feFlood952" /> |
| 823 | + <feComposite |
| 824 | + in="flood" |
| 825 | + in2="SourceGraphic" |
| 826 | + operator="in" |
| 827 | + result="composite1" |
| 828 | + id="feComposite954" /> |
| 829 | + <feGaussianBlur |
| 830 | + in="composite1" |
| 831 | + stdDeviation="4" |
| 832 | + result="blur" |
| 833 | + id="feGaussianBlur956" /> |
| 834 | + <feOffset |
| 835 | + dx="0" |
| 836 | + dy="1" |
| 837 | + result="offset" |
| 838 | + id="feOffset958" /> |
| 839 | + <feComposite |
| 840 | + in="SourceGraphic" |
| 841 | + in2="offset" |
| 842 | + operator="over" |
| 843 | + result="composite2" |
| 844 | + id="feComposite960" /> |
| 845 | + </filter> |
| 846 | + <linearGradient |
| 847 | + id="Background"> |
| 848 | + <stop |
| 849 | + id="stop4178" |
| 850 | + offset="0" |
| 851 | + style="stop-color:#22779e;stop-opacity:1" /> |
| 852 | + <stop |
| 853 | + id="stop4180" |
| 854 | + offset="1" |
| 855 | + style="stop-color:#2991c0;stop-opacity:1" /> |
| 856 | + </linearGradient> |
| 857 | + <clipPath |
| 858 | + clipPathUnits="userSpaceOnUse" |
| 859 | + id="clipPath873"> |
| 860 | + <g |
| 861 | + transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)" |
| 862 | + id="g875" |
| 863 | + inkscape:label="Layer 1" |
| 864 | + style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"> |
| 865 | + <path |
| 866 | + style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline" |
| 867 | + d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z" |
| 868 | + id="path877" |
| 869 | + inkscape:connector-curvature="0" |
| 870 | + sodipodi:nodetypes="sssssssss" /> |
| 871 | + </g> |
| 872 | + </clipPath> |
| 873 | + <filter |
| 874 | + inkscape:collect="always" |
| 875 | + id="filter891" |
| 876 | + inkscape:label="Badge Shadow"> |
| 877 | + <feGaussianBlur |
| 878 | + inkscape:collect="always" |
| 879 | + stdDeviation="0.71999962" |
| 880 | + id="feGaussianBlur893" /> |
| 881 | + </filter> |
| 882 | + <style |
| 883 | + id="style867" |
| 884 | + type="text/css"><![CDATA[ |
| 885 | + .fil0 {fill:#1F1A17} |
| 886 | + ]]></style> |
| 887 | + <linearGradient |
| 888 | + inkscape:collect="always" |
| 889 | + xlink:href="#linearGradient4439" |
| 890 | + id="linearGradient908" |
| 891 | + x1="-220" |
| 892 | + y1="731.29077" |
| 893 | + x2="-220" |
| 894 | + y2="635.29077" |
| 895 | + gradientUnits="userSpaceOnUse" /> |
| 896 | + <clipPath |
| 897 | + id="clipPath16"> |
| 898 | + <path |
| 899 | + id="path18" |
| 900 | + d="m -9,-9 614,0 0,231 -614,0 0,-231 z" /> |
| 901 | + </clipPath> |
| 902 | + <clipPath |
| 903 | + id="clipPath116"> |
| 904 | + <path |
| 905 | + id="path118" |
| 906 | + d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129" /> |
| 907 | + </clipPath> |
| 908 | + <clipPath |
| 909 | + id="clipPath128"> |
| 910 | + <path |
| 911 | + id="path130" |
| 912 | + d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129" /> |
| 913 | + </clipPath> |
| 914 | + <linearGradient |
| 915 | + id="linearGradient3850" |
| 916 | + inkscape:collect="always"> |
| 917 | + <stop |
| 918 | + id="stop3852" |
| 919 | + offset="0" |
| 920 | + style="stop-color:#000000;stop-opacity:1;" /> |
| 921 | + <stop |
| 922 | + id="stop3854" |
| 923 | + offset="1" |
| 924 | + style="stop-color:#000000;stop-opacity:0;" /> |
| 925 | + </linearGradient> |
| 926 | + <clipPath |
| 927 | + clipPathUnits="userSpaceOnUse" |
| 928 | + id="clipPath3095"> |
| 929 | + <path |
| 930 | + d="m 976.648,389.551 -842.402,0 0,839.999 842.402,0 0,-839.999" |
| 931 | + id="path3097" |
| 932 | + inkscape:connector-curvature="0" /> |
| 933 | + </clipPath> |
| 934 | + <linearGradient |
| 935 | + id="linearGradient4439" |
| 936 | + inkscape:collect="always"> |
| 937 | + <stop |
| 938 | + id="stop4441" |
| 939 | + offset="0" |
| 940 | + style="stop-color:#e6e6e6;stop-opacity:1" /> |
| 941 | + <stop |
| 942 | + id="stop4443" |
| 943 | + offset="1" |
| 944 | + style="stop-color:#f2f2f2;stop-opacity:1" /> |
| 945 | + </linearGradient> |
| 946 | + <clipPath |
| 947 | + clipPathUnits="userSpaceOnUse" |
| 948 | + id="clipPath3195"> |
| 949 | + <path |
| 950 | + d="m 611.836,756.738 -106.34,105.207 c -8.473,8.289 -13.617,20.102 -13.598,33.379 L 598.301,790.207 c -0.031,-13.418 5.094,-25.031 13.535,-33.469" |
| 951 | + id="path3197" |
| 952 | + inkscape:connector-curvature="0" /> |
| 953 | + </clipPath> |
| 954 | + <clipPath |
| 955 | + clipPathUnits="userSpaceOnUse" |
| 956 | + id="clipPath3235"> |
| 957 | + <path |
| 958 | + d="m 1095.64,1501.81 c 35.46,-35.07 70.89,-70.11 106.35,-105.17 4.4,-4.38 7.11,-10.53 7.11,-17.55 l -106.37,105.21 c 0,7 -2.71,13.11 -7.09,17.51" |
| 959 | + id="path3237" |
| 960 | + inkscape:connector-curvature="0" /> |
| 961 | + </clipPath> |
| 962 | + <clipPath |
| 963 | + id="clipPath4591" |
| 964 | + clipPathUnits="userSpaceOnUse"> |
| 965 | + <path |
| 966 | + inkscape:connector-curvature="0" |
| 967 | + d="m 1106.6009,730.43734 -0.036,21.648 c -0.01,3.50825 -2.8675,6.61375 -6.4037,6.92525 l -83.6503,7.33162 c -3.5205,0.30763 -6.3812,-2.29987 -6.3671,-5.8145 l 0.036,-21.6475 20.1171,-1.76662 -0.011,4.63775 c 0,1.83937 1.4844,3.19925 3.3262,3.0395 l 49.5274,-4.33975 c 1.8425,-0.166 3.3425,-1.78125 3.3538,-3.626 l 0.01,-4.63025 20.1,-1.7575" |
| 968 | + style="fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none" |
| 969 | + id="path4593" /> |
| 970 | + </clipPath> |
| 971 | + <radialGradient |
| 972 | + gradientUnits="userSpaceOnUse" |
| 973 | + gradientTransform="matrix(-1.4333926,-2.2742838,1.1731823,-0.73941125,-174.08025,98.374394)" |
| 974 | + r="20.40658" |
| 975 | + fy="93.399292" |
| 976 | + fx="-26.508606" |
| 977 | + cy="93.399292" |
| 978 | + cx="-26.508606" |
| 979 | + id="radialGradient3856" |
| 980 | + xlink:href="#linearGradient3850" |
| 981 | + inkscape:collect="always" /> |
| 982 | + <linearGradient |
| 983 | + gradientTransform="translate(-318.48033,212.32022)" |
| 984 | + gradientUnits="userSpaceOnUse" |
| 985 | + y2="993.19702" |
| 986 | + x2="-51.879555" |
| 987 | + y1="593.11615" |
| 988 | + x1="348.20132" |
| 989 | + id="linearGradient3895" |
| 990 | + xlink:href="#linearGradient3850" |
| 991 | + inkscape:collect="always" /> |
| 992 | + <clipPath |
| 993 | + id="clipPath3906" |
| 994 | + clipPathUnits="userSpaceOnUse"> |
| 995 | + <rect |
| 996 | + transform="scale(1,-1)" |
| 997 | + style="opacity:0.8;color:#000000;fill:#ff00ff;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
| 998 | + id="rect3908" |
| 999 | + width="1019.1371" |
| 1000 | + height="1019.1371" |
| 1001 | + x="357.9816" |
| 1002 | + y="-1725.8152" /> |
| 1003 | + </clipPath> |
| 1004 | + </defs> |
| 1005 | + <sodipodi:namedview |
| 1006 | + id="base" |
| 1007 | + pagecolor="#ffffff" |
| 1008 | + bordercolor="#666666" |
| 1009 | + borderopacity="1.0" |
| 1010 | + inkscape:pageopacity="0.0" |
| 1011 | + inkscape:pageshadow="2" |
| 1012 | + inkscape:zoom="4.0745361" |
| 1013 | + inkscape:cx="52.997123" |
| 1014 | + inkscape:cy="44.931814" |
| 1015 | + inkscape:document-units="px" |
| 1016 | + inkscape:current-layer="layer3" |
| 1017 | + showgrid="false" |
| 1018 | + fit-margin-top="0" |
| 1019 | + fit-margin-left="0" |
| 1020 | + fit-margin-right="0" |
| 1021 | + fit-margin-bottom="0" |
| 1022 | + inkscape:window-width="1920" |
| 1023 | + inkscape:window-height="1029" |
| 1024 | + inkscape:window-x="0" |
| 1025 | + inkscape:window-y="24" |
| 1026 | + inkscape:window-maximized="1" |
| 1027 | + showborder="true" |
| 1028 | + showguides="true" |
| 1029 | + inkscape:guide-bbox="true" |
| 1030 | + inkscape:showpageshadow="false" |
| 1031 | + inkscape:snap-global="true" |
| 1032 | + inkscape:snap-bbox="true" |
| 1033 | + inkscape:bbox-paths="true" |
| 1034 | + inkscape:bbox-nodes="true" |
| 1035 | + inkscape:snap-bbox-edge-midpoints="true" |
| 1036 | + inkscape:snap-bbox-midpoints="true" |
| 1037 | + inkscape:object-paths="true" |
| 1038 | + inkscape:snap-intersection-paths="true" |
| 1039 | + inkscape:object-nodes="true" |
| 1040 | + inkscape:snap-smooth-nodes="true" |
| 1041 | + inkscape:snap-midpoints="true" |
| 1042 | + inkscape:snap-object-midpoints="true" |
| 1043 | + inkscape:snap-center="true"> |
| 1044 | + <inkscape:grid |
| 1045 | + type="xygrid" |
| 1046 | + id="grid821" /> |
| 1047 | + <sodipodi:guide |
| 1048 | + orientation="1,0" |
| 1049 | + position="16,48" |
| 1050 | + id="guide823" /> |
| 1051 | + <sodipodi:guide |
| 1052 | + orientation="0,1" |
| 1053 | + position="64,80" |
| 1054 | + id="guide825" /> |
| 1055 | + <sodipodi:guide |
| 1056 | + orientation="1,0" |
| 1057 | + position="80,40" |
| 1058 | + id="guide827" /> |
| 1059 | + <sodipodi:guide |
| 1060 | + orientation="0,1" |
| 1061 | + position="64,16" |
| 1062 | + id="guide829" /> |
| 1063 | + </sodipodi:namedview> |
| 1064 | + <metadata |
| 1065 | + id="metadata6522"> |
| 1066 | + <rdf:RDF> |
| 1067 | + <cc:Work |
| 1068 | + rdf:about=""> |
| 1069 | + <dc:format>image/svg+xml</dc:format> |
| 1070 | + <dc:type |
| 1071 | + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
| 1072 | + <dc:title></dc:title> |
| 1073 | + </cc:Work> |
| 1074 | + </rdf:RDF> |
| 1075 | + </metadata> |
| 1076 | + <g |
| 1077 | + inkscape:label="BACKGROUND" |
| 1078 | + inkscape:groupmode="layer" |
| 1079 | + id="layer1" |
| 1080 | + transform="translate(268,-635.29076)" |
| 1081 | + style="display:inline"> |
| 1082 | + <path |
| 1083 | + style="fill:url(#linearGradient908);fill-opacity:1.0;stroke:none;display:inline;filter:url(#filter1121)" |
| 1084 | + d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z" |
| 1085 | + id="path6455" |
| 1086 | + inkscape:connector-curvature="0" |
| 1087 | + sodipodi:nodetypes="sssssssss" /> |
| 1088 | + </g> |
| 1089 | + <g |
| 1090 | + inkscape:groupmode="layer" |
| 1091 | + id="layer3" |
| 1092 | + inkscape:label="PLACE YOUR PICTOGRAM HERE" |
| 1093 | + style="display:inline"> |
| 1094 | + <g |
| 1095 | + id="g4115" |
| 1096 | + transform="matrix(0.37621289,0,0,0.37621289,-48.453493,-64.869307)" |
| 1097 | + style="filter:url(#filter950)"> |
| 1098 | + <path |
| 1099 | + id="path4127" |
| 1100 | + d="m 334.77,339.54 c -9.078,4.732 -56.106,24.068 -66.118,29.287 -10.012,5.221 -15.574,5.17 -23.483,1.389 -7.909,-3.781 -57.955,-23.996 -66.97,-28.305 -4.506,-2.154 -6.875,-3.971 -6.875,-5.688 l 0,-17.195 c 0,0 65.153,-14.184 75.672,-17.957 10.518,-3.774 14.167,-3.91 23.118,-0.631 8.952,3.279 62.474,12.936 71.321,16.176 0,0 -0.004,15.357 -0.004,16.951 0.001,1.7 -2.04,3.565 -6.661,5.973 z" |
| 1101 | + inkscape:connector-curvature="0" |
| 1102 | + style="fill:#a41e11" /> |
| 1103 | + <path |
| 1104 | + id="path4129" |
| 1105 | + d="m 334.77,322.34 c -9.078,4.73 -56.106,24.068 -66.118,29.287 -10.012,5.221 -15.574,5.17 -23.483,1.389 -7.91,-3.779 -57.955,-23.998 -66.97,-28.305 -9.015,-4.309 -9.204,-7.275 -0.348,-10.742 8.855,-3.469 58.626,-22.996 69.146,-26.77 10.518,-3.772 14.167,-3.91 23.118,-0.63 8.952,3.279 55.699,21.886 64.545,25.126 8.848,3.243 9.188,5.913 0.11,10.645 z" |
| 1106 | + inkscape:connector-curvature="0" |
| 1107 | + style="fill:#d82c20" /> |
| 1108 | + <path |
| 1109 | + id="path4131" |
| 1110 | + d="m 334.77,311.5 c -9.078,4.732 -56.106,24.068 -66.118,29.289 -10.012,5.219 -15.574,5.168 -23.483,1.387 -7.91,-3.779 -57.955,-23.996 -66.97,-28.305 -4.506,-2.154 -6.875,-3.969 -6.875,-5.686 l 0,-17.197 c 0,0 65.153,-14.183 75.672,-17.957 10.518,-3.773 14.167,-3.91 23.118,-0.631 8.952,3.279 62.474,12.934 71.321,16.175 0,0 -0.004,15.357 -0.004,16.953 0.001,1.699 -2.04,3.564 -6.661,5.972 z" |
| 1111 | + inkscape:connector-curvature="0" |
| 1112 | + style="fill:#a41e11" /> |
| 1113 | + <path |
| 1114 | + id="path4133" |
| 1115 | + d="m 334.77,294.3 c -9.078,4.732 -56.106,24.068 -66.118,29.289 -10.012,5.219 -15.574,5.168 -23.483,1.387 -7.91,-3.779 -57.955,-23.997 -66.97,-28.305 -9.015,-4.308 -9.204,-7.274 -0.348,-10.743 8.855,-3.467 58.626,-22.995 69.146,-26.768 10.518,-3.773 14.167,-3.91 23.118,-0.631 8.952,3.279 55.699,21.885 64.545,25.126 8.848,3.242 9.188,5.913 0.11,10.645 z" |
| 1116 | + inkscape:connector-curvature="0" |
| 1117 | + style="fill:#d82c20" /> |
| 1118 | + <path |
| 1119 | + id="path4135" |
| 1120 | + d="m 334.77,282.42 c -9.078,4.732 -56.106,24.069 -66.118,29.29 -10.012,5.219 -15.574,5.168 -23.483,1.387 -7.91,-3.779 -57.955,-23.997 -66.97,-28.305 -4.506,-2.154 -6.875,-3.97 -6.875,-5.686 l 0,-17.197 c 0,0 65.153,-14.183 75.672,-17.956 10.518,-3.774 14.167,-3.91 23.118,-0.631 8.952,3.279 62.474,12.934 71.321,16.175 0,0 -0.004,15.357 -0.004,16.952 0.001,1.698 -2.04,3.563 -6.661,5.971 z" |
| 1121 | + inkscape:connector-curvature="0" |
| 1122 | + style="fill:#a41e11" /> |
| 1123 | + <path |
| 1124 | + id="path4137" |
| 1125 | + d="m 334.77,265.22 c -9.078,4.732 -56.106,24.069 -66.118,29.289 -10.012,5.219 -15.574,5.168 -23.483,1.388 -7.909,-3.78 -57.955,-23.997 -66.97,-28.305 -9.015,-4.308 -9.204,-7.275 -0.348,-10.743 8.855,-3.468 58.626,-22.994 69.146,-26.768 10.518,-3.774 14.167,-3.91 23.118,-0.63 8.952,3.279 55.699,21.885 64.545,25.126 8.848,3.24 9.188,5.912 0.11,10.643 z" |
| 1126 | + inkscape:connector-curvature="0" |
| 1127 | + style="fill:#d82c20" /> |
| 1128 | + <polygon |
| 1129 | + id="polygon4139" |
| 1130 | + points="247.17,236.13 259.06,240.78 270.27,237.11 267.24,244.38 278.67,248.66 263.93,250.19 260.63,258.13 255.3,249.27 238.28,247.74 250.98,243.16 " |
| 1131 | + style="fill:#ffffff" /> |
| 1132 | + <polygon |
| 1133 | + id="polygon4141" |
| 1134 | + points="259.75,287.18 232.24,275.77 271.66,269.72 " |
| 1135 | + style="fill:#ffffff" /> |
| 1136 | + <ellipse |
| 1137 | + id="ellipse4143" |
| 1138 | + cy="261.23999" |
| 1139 | + ry="8.1669998" |
| 1140 | + rx="21.069" |
| 1141 | + cx="221.61" |
| 1142 | + d="m 242.679,261.23999 c 0,4.51051 -9.43291,8.167 -21.069,8.167 -11.63609,0 -21.069,-3.65649 -21.069,-8.167 0,-4.51051 9.43291,-8.167 21.069,-8.167 11.63609,0 21.069,3.65649 21.069,8.167 z" |
| 1143 | + sodipodi:cx="221.61" |
| 1144 | + sodipodi:cy="261.23999" |
| 1145 | + sodipodi:rx="21.069" |
| 1146 | + sodipodi:ry="8.1669998" |
| 1147 | + style="fill:#ffffff" /> |
| 1148 | + <polygon |
| 1149 | + id="polygon4145" |
| 1150 | + points="296.09,250.83 319.42,260.05 296.11,269.26 " |
| 1151 | + style="fill:#7a0c00" /> |
| 1152 | + <polygon |
| 1153 | + id="polygon4147" |
| 1154 | + points="296.11,269.26 293.58,270.25 270.28,261.04 296.09,250.83 " |
| 1155 | + style="fill:#ad2115" /> |
| 1156 | + </g> |
| 1157 | + </g> |
| 1158 | + <g |
| 1159 | + inkscape:groupmode="layer" |
| 1160 | + id="layer2" |
| 1161 | + inkscape:label="BADGE" |
| 1162 | + style="display:none" |
| 1163 | + sodipodi:insensitive="true"> |
| 1164 | + <g |
| 1165 | + style="display:inline" |
| 1166 | + transform="translate(-340.00001,-581)" |
| 1167 | + id="g4394" |
| 1168 | + clip-path="none"> |
| 1169 | + <g |
| 1170 | + id="g855"> |
| 1171 | + <g |
| 1172 | + inkscape:groupmode="maskhelper" |
| 1173 | + id="g870" |
| 1174 | + clip-path="url(#clipPath873)" |
| 1175 | + style="opacity:0.6;filter:url(#filter891)"> |
| 1176 | + <path |
| 1177 | + transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)" |
| 1178 | + d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z" |
| 1179 | + sodipodi:ry="12" |
| 1180 | + sodipodi:rx="12" |
| 1181 | + sodipodi:cy="552.36218" |
| 1182 | + sodipodi:cx="252" |
| 1183 | + id="path844" |
| 1184 | + style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
| 1185 | + sodipodi:type="arc" /> |
| 1186 | + </g> |
| 1187 | + <g |
| 1188 | + id="g862"> |
| 1189 | + <path |
| 1190 | + sodipodi:type="arc" |
| 1191 | + style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
| 1192 | + id="path4398" |
| 1193 | + sodipodi:cx="252" |
| 1194 | + sodipodi:cy="552.36218" |
| 1195 | + sodipodi:rx="12" |
| 1196 | + sodipodi:ry="12" |
| 1197 | + d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z" |
| 1198 | + transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" /> |
| 1199 | + <path |
| 1200 | + transform="matrix(1.25,0,0,1.25,33,-100.45273)" |
| 1201 | + d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z" |
| 1202 | + sodipodi:ry="12" |
| 1203 | + sodipodi:rx="12" |
| 1204 | + sodipodi:cy="552.36218" |
| 1205 | + sodipodi:cx="252" |
| 1206 | + id="path4400" |
| 1207 | + style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
| 1208 | + sodipodi:type="arc" /> |
| 1209 | + <path |
| 1210 | + sodipodi:type="star" |
| 1211 | + style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
| 1212 | + id="path4459" |
| 1213 | + sodipodi:sides="5" |
| 1214 | + sodipodi:cx="666.19574" |
| 1215 | + sodipodi:cy="589.50385" |
| 1216 | + sodipodi:r1="7.2431178" |
| 1217 | + sodipodi:r2="4.3458705" |
| 1218 | + sodipodi:arg1="1.0471976" |
| 1219 | + sodipodi:arg2="1.6755161" |
| 1220 | + inkscape:flatsided="false" |
| 1221 | + inkscape:rounded="0.1" |
| 1222 | + inkscape:randomized="0" |
| 1223 | + d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 -0.18379,0.41279 0.0427,4.27917 -0.34859,4.5051 z" |
| 1224 | + transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" /> |
| 1225 | + </g> |
| 1226 | + </g> |
| 1227 | + </g> |
| 1228 | + </g> |
| 1229 | +</svg> |
| 1230 | |
| 1231 | === added file 'metadata.yaml' |
| 1232 | --- metadata.yaml 1970-01-01 00:00:00 +0000 |
| 1233 | +++ metadata.yaml 2015-05-21 17:29:23 +0000 |
| 1234 | @@ -0,0 +1,21 @@ |
| 1235 | +name: redis |
| 1236 | +maintainer: Juju UI Team <juju-gui@lists.ubuntu.com> |
| 1237 | +summary: Persistent key-value database with network interface |
| 1238 | +description: | |
| 1239 | + Redis is a key-value database in a similar vein to memcache but the |
| 1240 | + dataset is non-volatile. Redis additionally provides native support |
| 1241 | + for atomically manipulating and querying data structures such as lists |
| 1242 | + and sets. The dataset is stored entirely in memory and periodically |
| 1243 | + flushed to disk. |
| 1244 | + This charm supports data replication and provides both the master and the |
| 1245 | + slave services. |
| 1246 | +tags: |
| 1247 | + - databases |
| 1248 | +provides: |
| 1249 | + master: |
| 1250 | + interface: redis |
| 1251 | + db: |
| 1252 | + interface: redis |
| 1253 | +requires: |
| 1254 | + slave: |
| 1255 | + interface: redis |
| 1256 | |
| 1257 | === added file 'test-requirements.pip' |
| 1258 | --- test-requirements.pip 1970-01-01 00:00:00 +0000 |
| 1259 | +++ test-requirements.pip 2015-05-21 17:29:23 +0000 |
| 1260 | @@ -0,0 +1,14 @@ |
| 1261 | +# Copyright 2015 Canonical Ltd. |
| 1262 | +# Licensed under the GPLv3, see copyright file for details. |
| 1263 | + |
| 1264 | +amulet==1.10.1 |
| 1265 | +bzr==2.6.0 # Required by juju-deployer. |
| 1266 | +charmhelpers==0.3.1 |
| 1267 | +coverage==3.7.1 |
| 1268 | +flake8==2.4.1 |
| 1269 | +juju-deployer==0.4.3 # Required by amulet. |
| 1270 | +mock==1.0.1 |
| 1271 | +nose==1.3.6 |
| 1272 | +PyYAML==3.11 # Required by charmhelpers. |
| 1273 | +six==1.9.0 # Required by charmhelpers. |
| 1274 | + |
| 1275 | |
| 1276 | === added directory 'tests' |
| 1277 | === added file 'tests/test_10_deploy.py' |
| 1278 | --- tests/test_10_deploy.py 1970-01-01 00:00:00 +0000 |
| 1279 | +++ tests/test_10_deploy.py 2015-05-21 17:29:23 +0000 |
| 1280 | @@ -0,0 +1,313 @@ |
| 1281 | +#!/usr/bin/env python3 |
| 1282 | + |
| 1283 | +# Copyright 2015 Canonical Ltd. |
| 1284 | +# Licensed under the GPLv3, see copyright file for details. |
| 1285 | + |
| 1286 | +"""Redis charm functional tests. |
| 1287 | + |
| 1288 | +These tests use the Amulet test helpers: |
| 1289 | +see https://jujucharms.com/docs/stable/tools-amulet |
| 1290 | +""" |
| 1291 | + |
| 1292 | +import itertools |
| 1293 | +from pkg_resources import resource_filename |
| 1294 | +import sys |
| 1295 | +import telnetlib |
| 1296 | +import unittest |
| 1297 | + |
| 1298 | +import amulet |
| 1299 | + |
| 1300 | +# Allow importing modules and packages from the hooks directory. |
| 1301 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1302 | + |
| 1303 | +import configfile |
| 1304 | + |
| 1305 | + |
| 1306 | +# Define the charm name. |
| 1307 | +CHARM_NAME = 'redis' |
| 1308 | + |
| 1309 | + |
| 1310 | +class RedisClient(object): |
| 1311 | + """A very simple and naive telnet redis client used for tests.""" |
| 1312 | + |
| 1313 | + def __init__(self, host, port=6379): |
| 1314 | + """Initialize the client.""" |
| 1315 | + self._host = host |
| 1316 | + self._port = port |
| 1317 | + self._client = None |
| 1318 | + |
| 1319 | + def connect(self, password=None): |
| 1320 | + """Connect to the client.""" |
| 1321 | + self._client = telnetlib.Telnet(self._host, self._port) |
| 1322 | + if password is not None: |
| 1323 | + self._client.write('AUTH {}\n'.format(password)) |
| 1324 | + response = self._readline() |
| 1325 | + if response != '+OK': |
| 1326 | + raise ValueError('authentication error: {}'.format(response)) |
| 1327 | + |
| 1328 | + def close(self): |
| 1329 | + """Close the client connection.""" |
| 1330 | + if self._client is not None: |
| 1331 | + self._client.close() |
| 1332 | + self._client = None |
| 1333 | + |
| 1334 | + def set(self, key, value): |
| 1335 | + """Set a key in the redis database, with the given value.""" |
| 1336 | + self._client.write('SET {} {}\n'.format(key, value)) |
| 1337 | + response = self._readline() |
| 1338 | + if response != '+OK': |
| 1339 | + raise ValueError('unexpected response: {}'.format(response)) |
| 1340 | + |
| 1341 | + def get(self, key): |
| 1342 | + """Return the value corresponding to key from the redis database. |
| 1343 | + |
| 1344 | + Return None if the key is not found. |
| 1345 | + """ |
| 1346 | + self._client.write('GET {}\n'.format(key)) |
| 1347 | + response = self._readline() |
| 1348 | + if response == '$-1': |
| 1349 | + return None |
| 1350 | + return self._readline() |
| 1351 | + |
| 1352 | + def _readline(self): |
| 1353 | + """Read next line from the client connection.""" |
| 1354 | + return self._client.read_until('\r\n').strip() |
| 1355 | + |
| 1356 | + |
| 1357 | +_counter = itertools.count() |
| 1358 | + |
| 1359 | + |
| 1360 | +def get_service_name(): |
| 1361 | + """Return an incremental redis service name.""" |
| 1362 | + return 'redis{}'.format(next(_counter)) |
| 1363 | + |
| 1364 | + |
| 1365 | +def deploy(options=None): |
| 1366 | + """Deploy one unit of the given service using the redis charm. |
| 1367 | + |
| 1368 | + Return the Amulet deployment and the unit object. |
| 1369 | + """ |
| 1370 | + deployment = amulet.Deployment(series='trusty') |
| 1371 | + service_name = get_service_name() |
| 1372 | + deployment.add(service_name, charm=CHARM_NAME) |
| 1373 | + if options is not None: |
| 1374 | + deployment.configure(service_name, options) |
| 1375 | + deployment.expose(service_name) |
| 1376 | + try: |
| 1377 | + deployment.setup(timeout=900) |
| 1378 | + deployment.sentry.wait() |
| 1379 | + except amulet.helpers.TimeoutError: |
| 1380 | + amulet.raise_status( |
| 1381 | + amulet.FAIL, msg='Environment was not stood up in time.') |
| 1382 | + return deployment, deployment.sentry.unit[service_name + '/0'] |
| 1383 | + |
| 1384 | + |
| 1385 | +def deploy_master_slave(master_options=None, slave_options=None): |
| 1386 | + """Deploy two redis services related in a master-slave relationship. |
| 1387 | + |
| 1388 | + Return the Amulet deployment and the two unit objects. |
| 1389 | + """ |
| 1390 | + deployment = amulet.Deployment(series='trusty') |
| 1391 | + master, slave = get_service_name(), get_service_name() |
| 1392 | + deployment.add(master, charm=CHARM_NAME) |
| 1393 | + deployment.add(slave, charm=CHARM_NAME) |
| 1394 | + if master_options is not None: |
| 1395 | + deployment.configure(master, master_options) |
| 1396 | + if slave_options is not None: |
| 1397 | + deployment.configure(slave, slave_options) |
| 1398 | + deployment.relate(master + ':master', slave + ':slave') |
| 1399 | + deployment.expose(master) |
| 1400 | + deployment.expose(slave) |
| 1401 | + try: |
| 1402 | + deployment.setup(timeout=900) |
| 1403 | + deployment.sentry.wait() |
| 1404 | + except amulet.helpers.TimeoutError: |
| 1405 | + amulet.raise_status( |
| 1406 | + amulet.FAIL, msg='Environment was not stood up in time.') |
| 1407 | + units = deployment.sentry.unit |
| 1408 | + return deployment, units[master + '/0'], units[slave + '/0'] |
| 1409 | + |
| 1410 | + |
| 1411 | +class TestDeployment(unittest.TestCase): |
| 1412 | + |
| 1413 | + @classmethod |
| 1414 | + def setUpClass(cls): |
| 1415 | + # Set up the environment and deploy the charm. |
| 1416 | + cls.deployment, cls.unit = deploy() |
| 1417 | + |
| 1418 | + @classmethod |
| 1419 | + def tearDownClass(cls): |
| 1420 | + # Remove the redis service. |
| 1421 | + cls.deployment.remove_service(cls.unit.info['service']) |
| 1422 | + |
| 1423 | + def test_config_file(self): |
| 1424 | + expected_content = ( |
| 1425 | + 'bind {}\n' |
| 1426 | + 'logfile /var/log/redis/redis-server.log\n' |
| 1427 | + 'loglevel notice\n' |
| 1428 | + 'port 6379\n' |
| 1429 | + ).format(self.unit.info['public-address']) |
| 1430 | + self.assertEqual( |
| 1431 | + expected_content, |
| 1432 | + self.unit.file_contents(configfile.REDIS_CONF)) |
| 1433 | + |
| 1434 | + def test_connection(self): |
| 1435 | + client = RedisClient(self.unit.info['public-address']) |
| 1436 | + client.connect() |
| 1437 | + self.addCleanup(client.close) |
| 1438 | + self.assertIsNone(client.get('my-key')) |
| 1439 | + client.set('my-key', 'my-value') |
| 1440 | + self.assertEqual('my-value', client.get('my-key')) |
| 1441 | + |
| 1442 | + |
| 1443 | +class TestDeploymentOptions(unittest.TestCase): |
| 1444 | + |
| 1445 | + options = { |
| 1446 | + 'port': 4242, |
| 1447 | + 'password': 'secret', |
| 1448 | + 'loglevel': 'verbose', |
| 1449 | + 'logfile': '/tmp/redis.log', |
| 1450 | + } |
| 1451 | + |
| 1452 | + @classmethod |
| 1453 | + def setUpClass(cls): |
| 1454 | + # Set up the environment and deploy the charm. |
| 1455 | + cls.deployment, cls.unit = deploy(options=cls.options) |
| 1456 | + |
| 1457 | + @classmethod |
| 1458 | + def tearDownClass(cls): |
| 1459 | + # Remove the redis service. |
| 1460 | + cls.deployment.remove_service(cls.unit.info['service']) |
| 1461 | + |
| 1462 | + def test_config_file(self): |
| 1463 | + expected_content = ( |
| 1464 | + 'bind {}\n' |
| 1465 | + 'logfile /tmp/redis.log\n' |
| 1466 | + 'loglevel verbose\n' |
| 1467 | + 'port 4242\n' |
| 1468 | + 'requirepass secret\n' |
| 1469 | + ).format(self.unit.info['public-address']) |
| 1470 | + self.assertEqual( |
| 1471 | + expected_content, |
| 1472 | + self.unit.file_contents(configfile.REDIS_CONF)) |
| 1473 | + |
| 1474 | + def test_connection(self): |
| 1475 | + client = RedisClient( |
| 1476 | + self.unit.info['public-address'], port=self.options['port']) |
| 1477 | + client.connect(password=self.options['password']) |
| 1478 | + self.addCleanup(client.close) |
| 1479 | + self.assertIsNone(client.get('my-key')) |
| 1480 | + client.set('my-key', 'my-value') |
| 1481 | + self.assertEqual('my-value', client.get('my-key')) |
| 1482 | + |
| 1483 | + |
| 1484 | +class TestMasterSlaveRelation(unittest.TestCase): |
| 1485 | + |
| 1486 | + @classmethod |
| 1487 | + def setUpClass(cls): |
| 1488 | + # Set up the environment and deploy the charm. |
| 1489 | + cls.deployment, cls.master, cls.slave = deploy_master_slave() |
| 1490 | + |
| 1491 | + @classmethod |
| 1492 | + def tearDownClass(cls): |
| 1493 | + # Remove the redis master and slave services. |
| 1494 | + cls.deployment.remove_service(cls.slave.info['service']) |
| 1495 | + cls.deployment.remove_service(cls.master.info['service']) |
| 1496 | + |
| 1497 | + def test_master_config_file(self): |
| 1498 | + expected_content = ( |
| 1499 | + 'bind {}\n' |
| 1500 | + 'logfile /var/log/redis/redis-server.log\n' |
| 1501 | + 'loglevel notice\n' |
| 1502 | + 'port 6379\n' |
| 1503 | + ).format(self.master.info['public-address']) |
| 1504 | + self.assertEqual( |
| 1505 | + expected_content, |
| 1506 | + self.master.file_contents(configfile.REDIS_CONF)) |
| 1507 | + |
| 1508 | + def test_slave_config_file(self): |
| 1509 | + expected_content = ( |
| 1510 | + 'bind {}\n' |
| 1511 | + 'logfile /var/log/redis/redis-server.log\n' |
| 1512 | + 'loglevel notice\n' |
| 1513 | + 'port 6379\n' |
| 1514 | + 'slaveof {} 6379\n' |
| 1515 | + ).format( |
| 1516 | + self.slave.info['public-address'], |
| 1517 | + self.master.info['public-address']) |
| 1518 | + self.assertEqual( |
| 1519 | + expected_content, |
| 1520 | + self.slave.file_contents(configfile.REDIS_CONF)) |
| 1521 | + |
| 1522 | + def test_connection(self): |
| 1523 | + master_client = RedisClient(self.master.info['public-address']) |
| 1524 | + master_client.connect() |
| 1525 | + self.addCleanup(master_client.close) |
| 1526 | + master_client.set('my-key', '42') |
| 1527 | + # Retrieve the value from the slave. |
| 1528 | + slave_client = RedisClient(self.slave.info['public-address']) |
| 1529 | + slave_client.connect() |
| 1530 | + self.addCleanup(slave_client.close) |
| 1531 | + self.assertEqual('42', slave_client.get('my-key')) |
| 1532 | + |
| 1533 | + |
| 1534 | +class TestMasterSlaveRelationOptions(unittest.TestCase): |
| 1535 | + |
| 1536 | + master_options = {'password': 'secret'} |
| 1537 | + slave_options = {'port': 4747, 'loglevel': 'warning'} |
| 1538 | + |
| 1539 | + @classmethod |
| 1540 | + def setUpClass(cls): |
| 1541 | + # Set up the environment and deploy the charm. |
| 1542 | + cls.deployment, cls.master, cls.slave = deploy_master_slave( |
| 1543 | + master_options=cls.master_options, |
| 1544 | + slave_options=cls.slave_options) |
| 1545 | + |
| 1546 | + @classmethod |
| 1547 | + def tearDownClass(cls): |
| 1548 | + # Remove the redis master and slave services. |
| 1549 | + cls.deployment.remove_service(cls.slave.info['service']) |
| 1550 | + cls.deployment.remove_service(cls.master.info['service']) |
| 1551 | + |
| 1552 | + def test_master_config_file(self): |
| 1553 | + expected_content = ( |
| 1554 | + 'bind {}\n' |
| 1555 | + 'logfile /var/log/redis/redis-server.log\n' |
| 1556 | + 'loglevel notice\n' |
| 1557 | + 'port 6379\n' |
| 1558 | + 'requirepass secret\n' |
| 1559 | + ).format(self.master.info['public-address']) |
| 1560 | + self.assertEqual( |
| 1561 | + expected_content, |
| 1562 | + self.master.file_contents(configfile.REDIS_CONF)) |
| 1563 | + |
| 1564 | + def test_slave_config_file(self): |
| 1565 | + expected_content = ( |
| 1566 | + 'bind {}\n' |
| 1567 | + 'logfile /var/log/redis/redis-server.log\n' |
| 1568 | + 'loglevel warning\n' |
| 1569 | + 'masterauth secret\n' |
| 1570 | + 'port 4747\n' |
| 1571 | + 'slaveof {} 6379\n' |
| 1572 | + ).format( |
| 1573 | + self.slave.info['public-address'], |
| 1574 | + self.master.info['public-address']) |
| 1575 | + self.assertEqual( |
| 1576 | + expected_content, |
| 1577 | + self.slave.file_contents(configfile.REDIS_CONF)) |
| 1578 | + |
| 1579 | + def test_connection(self): |
| 1580 | + master_client = RedisClient(self.master.info['public-address']) |
| 1581 | + master_client.connect(password=self.master_options['password']) |
| 1582 | + self.addCleanup(master_client.close) |
| 1583 | + master_client.set('my-key', '42') |
| 1584 | + # Retrieve the value from the slave. |
| 1585 | + slave_client = RedisClient( |
| 1586 | + self.slave.info['public-address'], port=self.slave_options['port']) |
| 1587 | + slave_client.connect() |
| 1588 | + self.addCleanup(slave_client.close) |
| 1589 | + self.assertEqual('42', slave_client.get('my-key')) |
| 1590 | + |
| 1591 | + |
| 1592 | +if __name__ == '__main__': |
| 1593 | + unittest.main(verbosity=2) |
| 1594 | |
| 1595 | === added file 'tests/tests.yaml' |
| 1596 | --- tests/tests.yaml 1970-01-01 00:00:00 +0000 |
| 1597 | +++ tests/tests.yaml 2015-05-21 17:29:23 +0000 |
| 1598 | @@ -0,0 +1,10 @@ |
| 1599 | +bootstrap: true |
| 1600 | +reset: false |
| 1601 | +tests: "none" |
| 1602 | +packages: |
| 1603 | + - build-essential |
| 1604 | + - charm-tools |
| 1605 | + - python-dev |
| 1606 | + - python-pip |
| 1607 | + - python-virtualenv |
| 1608 | + - rsync |
| 1609 | |
| 1610 | === added directory 'unit_tests' |
| 1611 | === added file 'unit_tests/test_configfile.py' |
| 1612 | --- unit_tests/test_configfile.py 1970-01-01 00:00:00 +0000 |
| 1613 | +++ unit_tests/test_configfile.py 2015-05-21 17:29:23 +0000 |
| 1614 | @@ -0,0 +1,90 @@ |
| 1615 | +# Copyright 2015 Canonical Ltd. |
| 1616 | +# Licensed under the GPLv3, see copyright file for details. |
| 1617 | + |
| 1618 | +import os |
| 1619 | +from pkg_resources import resource_filename |
| 1620 | +import shutil |
| 1621 | +import sys |
| 1622 | +import tempfile |
| 1623 | +import unittest |
| 1624 | + |
| 1625 | +import mock |
| 1626 | + |
| 1627 | +# Allow importing modules and packages from the hooks directory. |
| 1628 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1629 | + |
| 1630 | +import configfile |
| 1631 | + |
| 1632 | + |
| 1633 | +class TestIncludeConfig(unittest.TestCase): |
| 1634 | + |
| 1635 | + def test_success(self): |
| 1636 | + conf = tempfile.NamedTemporaryFile(delete=False) |
| 1637 | + self.addCleanup(os.remove, conf.name) |
| 1638 | + # Also remove the backup file created in the process. |
| 1639 | + self.addCleanup(os.remove, conf.name + '.bak') |
| 1640 | + conf.write('content\n') |
| 1641 | + conf.close() |
| 1642 | + with mock.patch('configfile.DEFAULT_REDIS_CONF', conf.name): |
| 1643 | + configfile.include_config('/my/customized/config') |
| 1644 | + expected_content = 'content\ninclude /my/customized/config\n' |
| 1645 | + self.assertEqual(expected_content, open(conf.name, 'r').read()) |
| 1646 | + |
| 1647 | + def test_not_found(self): |
| 1648 | + with mock.patch('configfile.DEFAULT_REDIS_CONF', '/no/such/file'): |
| 1649 | + with self.assertRaises(IOError) as ctx: |
| 1650 | + configfile.include_config('/my/customized/config') |
| 1651 | + expected_error = "[Errno 2] No such file or directory: '/no/such/file'" |
| 1652 | + self.assertEqual(expected_error, bytes(ctx.exception)) |
| 1653 | + |
| 1654 | + |
| 1655 | +class TestWrite(unittest.TestCase): |
| 1656 | + |
| 1657 | + def make_target(self, content=None): |
| 1658 | + """Return a target file path in a temporary directory. |
| 1659 | + |
| 1660 | + If content is not None, also create the target file itself with the |
| 1661 | + given content. |
| 1662 | + """ |
| 1663 | + playground = tempfile.mkdtemp() |
| 1664 | + self.addCleanup(shutil.rmtree, playground) |
| 1665 | + target = os.path.join(playground, 'target') |
| 1666 | + if content is not None: |
| 1667 | + with open(target, 'w') as target_file: |
| 1668 | + target_file.write(content) |
| 1669 | + return target |
| 1670 | + |
| 1671 | + def assert_file_content(self, target, content): |
| 1672 | + """Ensure a file exists with the given content.""" |
| 1673 | + self.assertTrue(os.path.isfile(target)) |
| 1674 | + self.assertEqual(content, open(target, 'r').read()) |
| 1675 | + |
| 1676 | + def test_unexisting_target(self): |
| 1677 | + target = self.make_target() |
| 1678 | + changed = configfile.write({'bind': '1.2.3.4', 'port': 4242}, target) |
| 1679 | + self.assertTrue(changed) |
| 1680 | + self.assert_file_content(target, 'bind 1.2.3.4\nport 4242\n') |
| 1681 | + self.assertFalse(os.path.exists(target + '.bak')) |
| 1682 | + |
| 1683 | + def test_existing_target(self): |
| 1684 | + target = self.make_target('original content') |
| 1685 | + changed = configfile.write({'bind': '1.2.3.4', 'port': 7000}, target) |
| 1686 | + self.assertTrue(changed) |
| 1687 | + self.assert_file_content(target, 'bind 1.2.3.4\nport 7000\n') |
| 1688 | + self.assert_file_content(target + '.bak', 'original content') |
| 1689 | + |
| 1690 | + def test_no_changes(self): |
| 1691 | + target = self.make_target('bind 1.2.3.4\n') |
| 1692 | + changed = configfile.write({'bind': '1.2.3.4'}, target) |
| 1693 | + self.assertFalse(changed) |
| 1694 | + self.assert_file_content(target, 'bind 1.2.3.4\n') |
| 1695 | + self.assertFalse(os.path.exists(target + '.bak')) |
| 1696 | + |
| 1697 | + def test_error(self): |
| 1698 | + target = self.make_target('original content') |
| 1699 | + os.chmod(target, 0) |
| 1700 | + self.addCleanup(os.chmod, target, 0666) |
| 1701 | + with self.assertRaises(IOError) as ctx: |
| 1702 | + configfile.write({'bind': '1.2.3.4'}, target) |
| 1703 | + expected_error = "[Errno 13] Permission denied: '{}'".format(target) |
| 1704 | + self.assertEqual(expected_error, bytes(ctx.exception)) |
| 1705 | |
| 1706 | === added file 'unit_tests/test_hookutils.py' |
| 1707 | --- unit_tests/test_hookutils.py 1970-01-01 00:00:00 +0000 |
| 1708 | +++ unit_tests/test_hookutils.py 2015-05-21 17:29:23 +0000 |
| 1709 | @@ -0,0 +1,58 @@ |
| 1710 | +# Copyright 2015 Canonical Ltd. |
| 1711 | +# Licensed under the GPLv3, see copyright file for details. |
| 1712 | + |
| 1713 | +from pkg_resources import resource_filename |
| 1714 | +import sys |
| 1715 | +import unittest |
| 1716 | + |
| 1717 | +from charmhelpers.core import hookenv |
| 1718 | +import mock |
| 1719 | + |
| 1720 | +# Allow importing modules and packages from the hooks directory. |
| 1721 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1722 | + |
| 1723 | +import hookutils |
| 1724 | + |
| 1725 | + |
| 1726 | +def _successful_hook(): |
| 1727 | + """An example successful hook used for tests.""" |
| 1728 | + hookenv.log('executing') |
| 1729 | + return 42 |
| 1730 | + |
| 1731 | + |
| 1732 | +def _failing_hook(): |
| 1733 | + """An example failing hook used for tests.""" |
| 1734 | + hookenv.log('failing') |
| 1735 | + raise TypeError |
| 1736 | + |
| 1737 | + |
| 1738 | +@mock.patch('charmhelpers.core.hookenv.hook_name') |
| 1739 | +@mock.patch('charmhelpers.core.hookenv.log') |
| 1740 | +class TestHookNameLogged(unittest.TestCase): |
| 1741 | + |
| 1742 | + def test_successful_hook(self, mock_log, mock_hook_name): |
| 1743 | + mock_hook_name.return_value = 'config-changed' |
| 1744 | + decorated = hookutils.hook_name_logged(_successful_hook) |
| 1745 | + result = decorated() |
| 1746 | + self.assertEqual(42, result) |
| 1747 | + mock_hook_name.assert_called_once_with() |
| 1748 | + self.assertEqual(3, mock_log.call_count) |
| 1749 | + mock_log.assert_has_calls([ |
| 1750 | + mock.call('>>> Entering hook: config-changed.'), |
| 1751 | + mock.call('executing'), |
| 1752 | + mock.call('<<< Exiting hook: config-changed.'), |
| 1753 | + ]) |
| 1754 | + |
| 1755 | + def test_failing_hook(self, mock_log, mock_hook_name): |
| 1756 | + mock_hook_name.return_value = 'start' |
| 1757 | + decorated = hookutils.hook_name_logged(_failing_hook) |
| 1758 | + with self.assertRaises(TypeError): |
| 1759 | + decorated() |
| 1760 | + mock_hook_name.assert_called_once_with() |
| 1761 | + # Even if the hook raised an exception, exiting it is still logged. |
| 1762 | + self.assertEqual(3, mock_log.call_count) |
| 1763 | + mock_log.assert_has_calls([ |
| 1764 | + mock.call('>>> Entering hook: start.'), |
| 1765 | + mock.call('failing'), |
| 1766 | + mock.call('<<< Exiting hook: start.') |
| 1767 | + ]) |
| 1768 | |
| 1769 | === added file 'unit_tests/test_relations.py' |
| 1770 | --- unit_tests/test_relations.py 1970-01-01 00:00:00 +0000 |
| 1771 | +++ unit_tests/test_relations.py 2015-05-21 17:29:23 +0000 |
| 1772 | @@ -0,0 +1,54 @@ |
| 1773 | +# Copyright 2015 Canonical Ltd. |
| 1774 | +# Licensed under the GPLv3, see copyright file for details. |
| 1775 | + |
| 1776 | +from pkg_resources import resource_filename |
| 1777 | +import sys |
| 1778 | +import unittest |
| 1779 | + |
| 1780 | +import mock |
| 1781 | + |
| 1782 | +# Allow importing modules and packages from the hooks directory. |
| 1783 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1784 | + |
| 1785 | +import relations |
| 1786 | + |
| 1787 | + |
| 1788 | +def patch_config(data): |
| 1789 | + """Patch the "charmhelpers.core.hookenv.config" function. |
| 1790 | + |
| 1791 | + The mocked function returns the given value. |
| 1792 | + """ |
| 1793 | + return mock.patch( |
| 1794 | + 'charmhelpers.core.hookenv.config', |
| 1795 | + lambda: data) |
| 1796 | + |
| 1797 | + |
| 1798 | +def patch_unit_get(value): |
| 1799 | + """Patch the "charmhelpers.core.hookenv.unit_get" function. |
| 1800 | + |
| 1801 | + The mocked function returns the given value. |
| 1802 | + """ |
| 1803 | + return mock.patch( |
| 1804 | + 'charmhelpers.core.hookenv.unit_get', |
| 1805 | + mock.Mock(return_value=value)) |
| 1806 | + |
| 1807 | + |
| 1808 | +@mock.patch('charmhelpers.core.hookenv.log', mock.Mock()) |
| 1809 | +class TestDbRelation(unittest.TestCase): |
| 1810 | + |
| 1811 | + def setUp(self): |
| 1812 | + relation_ids_path = 'charmhelpers.core.hookenv.relation_ids' |
| 1813 | + with mock.patch(relation_ids_path, mock.MagicMock()): |
| 1814 | + self.relation = relations.DbRelation() |
| 1815 | + |
| 1816 | + def test_provide_data(self): |
| 1817 | + with patch_config({'port': 4242, 'password': 'secret!'}): |
| 1818 | + with patch_unit_get('1.2.3.4') as mock_unit_get: |
| 1819 | + data = self.relation.provide_data() |
| 1820 | + expected_data = { |
| 1821 | + 'hostname': '1.2.3.4', |
| 1822 | + 'port': 4242, |
| 1823 | + 'password': 'secret!', |
| 1824 | + } |
| 1825 | + self.assertEqual(expected_data, data) |
| 1826 | + mock_unit_get.assert_called_once_with('public-address') |
| 1827 | |
| 1828 | === added file 'unit_tests/test_services.py' |
| 1829 | --- unit_tests/test_services.py 1970-01-01 00:00:00 +0000 |
| 1830 | +++ unit_tests/test_services.py 2015-05-21 17:29:23 +0000 |
| 1831 | @@ -0,0 +1,28 @@ |
| 1832 | +# Copyright 2015 Canonical Ltd. |
| 1833 | +# Licensed under the GPLv3, see copyright file for details. |
| 1834 | + |
| 1835 | +from pkg_resources import resource_filename |
| 1836 | +import sys |
| 1837 | +import unittest |
| 1838 | + |
| 1839 | +import mock |
| 1840 | + |
| 1841 | +# Allow importing modules and packages from the hooks directory. |
| 1842 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1843 | + |
| 1844 | +import services |
| 1845 | + |
| 1846 | + |
| 1847 | +@mock.patch('charmhelpers.core.hookenv.config') |
| 1848 | +@mock.patch('charmhelpers.core.hookenv.log', mock.Mock()) |
| 1849 | +@mock.patch('charmhelpers.core.hookenv.relation_ids', mock.MagicMock()) |
| 1850 | +@mock.patch('charmhelpers.core.services.base.ServiceManager') |
| 1851 | +class TestManage(unittest.TestCase): |
| 1852 | + |
| 1853 | + def test_services(self, mock_manager, mock_config): |
| 1854 | + services.manage() |
| 1855 | + self.assertEqual(1, mock_manager.call_count) |
| 1856 | + definitions = mock_manager.call_args[0][0] |
| 1857 | + service_names = [i['service'] for i in definitions] |
| 1858 | + self.assertEqual(['redis-master', 'redis-slave'], service_names) |
| 1859 | + mock_config.assert_called_once_with() |
| 1860 | |
| 1861 | === added file 'unit_tests/test_serviceutils.py' |
| 1862 | --- unit_tests/test_serviceutils.py 1970-01-01 00:00:00 +0000 |
| 1863 | +++ unit_tests/test_serviceutils.py 2015-05-21 17:29:23 +0000 |
| 1864 | @@ -0,0 +1,302 @@ |
| 1865 | +# Copyright 2015 Canonical Ltd. |
| 1866 | +# Licensed under the GPLv3, see copyright file for details. |
| 1867 | + |
| 1868 | +import contextlib |
| 1869 | +from pkg_resources import resource_filename |
| 1870 | +import sys |
| 1871 | +import unittest |
| 1872 | + |
| 1873 | +import mock |
| 1874 | + |
| 1875 | +# Allow importing modules and packages from the hooks directory. |
| 1876 | +sys.path.append(resource_filename(__name__, '../hooks')) |
| 1877 | + |
| 1878 | +import configfile |
| 1879 | +import serviceutils |
| 1880 | + |
| 1881 | + |
| 1882 | +def patch_service_running(value): |
| 1883 | + """Patch the "charmhelpers.core.host.service_running" function. |
| 1884 | + |
| 1885 | + The mocked function returns the given value. |
| 1886 | + """ |
| 1887 | + return mock.patch( |
| 1888 | + 'charmhelpers.core.host.service_running', |
| 1889 | + lambda service_name: value) |
| 1890 | + |
| 1891 | + |
| 1892 | +def patch_hook_name(value): |
| 1893 | + """Patch the "charmhelpers.core.hookenv.hook_name" function. |
| 1894 | + |
| 1895 | + The mocked function returns the given value. |
| 1896 | + """ |
| 1897 | + return mock.patch('charmhelpers.core.hookenv.hook_name', lambda: value) |
| 1898 | + |
| 1899 | + |
| 1900 | +@mock.patch('charmhelpers.core.host.service_start') |
| 1901 | +@mock.patch('charmhelpers.core.hookenv.log') |
| 1902 | +class TestServiceStart(unittest.TestCase): |
| 1903 | + |
| 1904 | + def test_not_running(self, mock_log, mock_service_start): |
| 1905 | + with patch_service_running(False): |
| 1906 | + serviceutils.service_start('foo') |
| 1907 | + mock_service_start.assert_called_once_with(serviceutils.SERVICE_NAME) |
| 1908 | + mock_log.assert_called_once_with('Starting service foo.') |
| 1909 | + |
| 1910 | + def test_already_running(self, mock_log, mock_service_start): |
| 1911 | + with patch_service_running(True): |
| 1912 | + serviceutils.service_start('foo') |
| 1913 | + self.assertFalse(mock_service_start.called) |
| 1914 | + self.assertFalse(mock_log.called) |
| 1915 | + |
| 1916 | + |
| 1917 | +@mock.patch('charmhelpers.core.host.service_stop') |
| 1918 | +@mock.patch('charmhelpers.core.hookenv.log') |
| 1919 | +class TestServiceStop(unittest.TestCase): |
| 1920 | + |
| 1921 | + def test_service_running_stop_hook(self, mock_log, mock_service_stop): |
| 1922 | + with patch_service_running(True): |
| 1923 | + with patch_hook_name('stop'): |
| 1924 | + serviceutils.service_stop('foo') |
| 1925 | + mock_service_stop.assert_called_once_with(serviceutils.SERVICE_NAME) |
| 1926 | + mock_log.assert_called_once_with('Stopping service foo.') |
| 1927 | + |
| 1928 | + def test_service_not_running_stop_hook(self, mock_log, mock_service_stop): |
| 1929 | + with patch_service_running(False): |
| 1930 | + with patch_hook_name('stop'): |
| 1931 | + serviceutils.service_stop('foo') |
| 1932 | + self.assertFalse(mock_service_stop.called) |
| 1933 | + self.assertFalse(mock_log.called) |
| 1934 | + |
| 1935 | + def test_service_running_other_hook(self, mock_log, mock_service_stop): |
| 1936 | + with patch_service_running(True): |
| 1937 | + with patch_hook_name('config-changed'): |
| 1938 | + serviceutils.service_stop('foo') |
| 1939 | + self.assertFalse(mock_service_stop.called) |
| 1940 | + self.assertFalse(mock_log.called) |
| 1941 | + |
| 1942 | + def test_service_not_running_other_hook(self, mock_log, mock_service_stop): |
| 1943 | + with patch_service_running(False): |
| 1944 | + with patch_hook_name('config-changed'): |
| 1945 | + serviceutils.service_stop('foo') |
| 1946 | + self.assertFalse(mock_service_stop.called) |
| 1947 | + self.assertFalse(mock_log.called) |
| 1948 | + |
| 1949 | + |
| 1950 | +def make_relation(data): |
| 1951 | + """Create and return a mock relation with the given data.""" |
| 1952 | + relation = type('Relation', (dict,), { |
| 1953 | + 'name': 'testing', |
| 1954 | + 'provide_data': lambda self: data, |
| 1955 | + })() |
| 1956 | + relation['testing'] = [data] |
| 1957 | + return relation |
| 1958 | + |
| 1959 | + |
| 1960 | +class TestWriteConfigFile(unittest.TestCase): |
| 1961 | + |
| 1962 | + @contextlib.contextmanager |
| 1963 | + def patch_all(self, configuration_changed=False): |
| 1964 | + """Mock all the external functions used by write_config_file.""" |
| 1965 | + mocks = { |
| 1966 | + 'log': mock.patch('charmhelpers.core.hookenv.log'), |
| 1967 | + 'relation_ids': mock.patch( |
| 1968 | + 'charmhelpers.core.hookenv.relation_ids', |
| 1969 | + mock.Mock(return_value=['rel-id'])), |
| 1970 | + 'relation_set': mock.patch( |
| 1971 | + 'charmhelpers.core.hookenv.relation_set'), |
| 1972 | + 'service_restart': mock.patch( |
| 1973 | + 'charmhelpers.core.host.service_restart'), |
| 1974 | + 'unit_get': mock.patch( |
| 1975 | + 'charmhelpers.core.hookenv.unit_get', |
| 1976 | + mock.Mock(return_value='1.2.3.4')), |
| 1977 | + 'write': mock.patch( |
| 1978 | + 'configfile.write', |
| 1979 | + mock.Mock(return_value=configuration_changed)) |
| 1980 | + } |
| 1981 | + # Note: nested is deprecated for good reasons which do not apply here. |
| 1982 | + # Used here to easily nest a dynamically generated list of context |
| 1983 | + # managers. |
| 1984 | + with contextlib.nested(*mocks.values()) as context_managers: |
| 1985 | + object_dict = dict(zip(mocks.keys(), context_managers)) |
| 1986 | + yield type('Mocks', (object,), object_dict) |
| 1987 | + |
| 1988 | + def test_configuration_changed(self): |
| 1989 | + config = { |
| 1990 | + 'logfile': '/path/to/logfile', |
| 1991 | + 'loglevel': 'debug', |
| 1992 | + 'password': '', |
| 1993 | + 'port': 4242, |
| 1994 | + } |
| 1995 | + callback = serviceutils.write_config_file(config) |
| 1996 | + with self.patch_all(configuration_changed=True) as mocks: |
| 1997 | + callback('foo') |
| 1998 | + mocks.write.assert_called_once_with({ |
| 1999 | + 'bind': '1.2.3.4', |
| 2000 | + 'logfile': '/path/to/logfile', |
| 2001 | + 'loglevel': 'debug', |
| 2002 | + 'port': 4242, |
| 2003 | + }, configfile.REDIS_CONF) |
| 2004 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2005 | + mocks.service_restart.assert_called_once_with( |
| 2006 | + serviceutils.SERVICE_NAME) |
| 2007 | + self.assertEqual(3, mocks.log.call_count) |
| 2008 | + mocks.log.assert_has_calls([ |
| 2009 | + mock.call('Retrieving service options.'), |
| 2010 | + mock.call('Writing configuration file for foo.'), |
| 2011 | + mock.call('Restarting service due to configuration change.') |
| 2012 | + ]) |
| 2013 | + |
| 2014 | + def test_configuration_changed_password(self): |
| 2015 | + config = { |
| 2016 | + 'logfile': '/path/to/logfile', |
| 2017 | + 'loglevel': 'debug', |
| 2018 | + 'password': 'secret!', |
| 2019 | + 'port': 4242, |
| 2020 | + } |
| 2021 | + callback = serviceutils.write_config_file(config) |
| 2022 | + with self.patch_all(configuration_changed=True) as mocks: |
| 2023 | + callback('foo') |
| 2024 | + mocks.write.assert_called_once_with({ |
| 2025 | + 'bind': '1.2.3.4', |
| 2026 | + 'logfile': '/path/to/logfile', |
| 2027 | + 'loglevel': 'debug', |
| 2028 | + 'port': 4242, |
| 2029 | + 'requirepass': 'secret!', |
| 2030 | + }, configfile.REDIS_CONF) |
| 2031 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2032 | + mocks.service_restart.assert_called_once_with( |
| 2033 | + serviceutils.SERVICE_NAME) |
| 2034 | + |
| 2035 | + def test_configuration_changed_relations(self): |
| 2036 | + config = { |
| 2037 | + 'logfile': '/path/to/logfile', |
| 2038 | + 'loglevel': 'debug', |
| 2039 | + 'password': 'secret!', |
| 2040 | + 'port': 4242, |
| 2041 | + } |
| 2042 | + data = { |
| 2043 | + 'hostname': '4.3.2.1', |
| 2044 | + 'port': 90, |
| 2045 | + } |
| 2046 | + db_relation = make_relation(data) |
| 2047 | + callback = serviceutils.write_config_file( |
| 2048 | + config, db_relation=db_relation) |
| 2049 | + with self.patch_all(configuration_changed=True) as mocks: |
| 2050 | + callback('foo') |
| 2051 | + mocks.write.assert_called_once_with({ |
| 2052 | + 'bind': '1.2.3.4', |
| 2053 | + 'logfile': '/path/to/logfile', |
| 2054 | + 'loglevel': 'debug', |
| 2055 | + 'port': 4242, |
| 2056 | + 'requirepass': 'secret!', |
| 2057 | + }, configfile.REDIS_CONF) |
| 2058 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2059 | + mocks.service_restart.assert_called_once_with( |
| 2060 | + serviceutils.SERVICE_NAME) |
| 2061 | + mocks.relation_ids.assert_called_once_with('testing') |
| 2062 | + mocks.relation_set.assert_called_once_with('rel-id', data) |
| 2063 | + |
| 2064 | + def test_configuration_unchanged_master(self): |
| 2065 | + config = { |
| 2066 | + 'logfile': '/path/to/logfile', |
| 2067 | + 'loglevel': 'debug', |
| 2068 | + 'password': '', |
| 2069 | + 'port': 4242, |
| 2070 | + } |
| 2071 | + callback = serviceutils.write_config_file(config) |
| 2072 | + with self.patch_all() as mocks: |
| 2073 | + callback('foo') |
| 2074 | + mocks.write.assert_called_once_with({ |
| 2075 | + 'bind': '1.2.3.4', |
| 2076 | + 'logfile': '/path/to/logfile', |
| 2077 | + 'loglevel': 'debug', |
| 2078 | + 'port': 4242, |
| 2079 | + }, configfile.REDIS_CONF) |
| 2080 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2081 | + self.assertFalse(mocks.service_restart.called) |
| 2082 | + self.assertEqual(3, mocks.log.call_count) |
| 2083 | + mocks.log.assert_has_calls([ |
| 2084 | + mock.call('Retrieving service options.'), |
| 2085 | + mock.call('Writing configuration file for foo.'), |
| 2086 | + mock.call('No changes detected in the configuration file.') |
| 2087 | + ]) |
| 2088 | + |
| 2089 | + def test_configuration_unchanged_slave(self): |
| 2090 | + config = { |
| 2091 | + 'logfile': '/path/to/logs', |
| 2092 | + 'loglevel': 'info', |
| 2093 | + 'password': ' ', |
| 2094 | + 'port': 4242, |
| 2095 | + } |
| 2096 | + slave_relation = make_relation({ |
| 2097 | + 'hostname': '4.3.2.1', |
| 2098 | + 'port': 4747, |
| 2099 | + }) |
| 2100 | + callback = serviceutils.write_config_file( |
| 2101 | + config, slave_relation=slave_relation) |
| 2102 | + with self.patch_all() as mocks: |
| 2103 | + callback('foo') |
| 2104 | + mocks.write.assert_called_once_with({ |
| 2105 | + 'bind': '1.2.3.4', |
| 2106 | + 'logfile': '/path/to/logs', |
| 2107 | + 'loglevel': 'info', |
| 2108 | + 'port': 4242, |
| 2109 | + 'slaveof': '4.3.2.1 4747' |
| 2110 | + }, configfile.REDIS_CONF) |
| 2111 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2112 | + self.assertFalse(mocks.service_restart.called) |
| 2113 | + self.assertEqual(4, mocks.log.call_count) |
| 2114 | + mocks.log.assert_has_calls([ |
| 2115 | + mock.call('Retrieving service options.'), |
| 2116 | + mock.call('Setting up slave relation.'), |
| 2117 | + mock.call('Writing configuration file for foo.'), |
| 2118 | + mock.call('No changes detected in the configuration file.') |
| 2119 | + ]) |
| 2120 | + |
| 2121 | + def test_configuration_unchanged_master_password(self): |
| 2122 | + config = { |
| 2123 | + 'logfile': '/path/to/logfile', |
| 2124 | + 'loglevel': 'debug', |
| 2125 | + 'password': 'secret!', |
| 2126 | + 'port': 42, |
| 2127 | + } |
| 2128 | + callback = serviceutils.write_config_file(config) |
| 2129 | + with self.patch_all() as mocks: |
| 2130 | + callback('foo') |
| 2131 | + mocks.write.assert_called_once_with({ |
| 2132 | + 'bind': '1.2.3.4', |
| 2133 | + 'logfile': '/path/to/logfile', |
| 2134 | + 'loglevel': 'debug', |
| 2135 | + 'port': 42, |
| 2136 | + 'requirepass': 'secret!', |
| 2137 | + }, configfile.REDIS_CONF) |
| 2138 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2139 | + self.assertFalse(mocks.service_restart.called) |
| 2140 | + |
| 2141 | + def test_configuration_unchanged_slave_password(self): |
| 2142 | + config = { |
| 2143 | + 'logfile': '/path/to/logs', |
| 2144 | + 'loglevel': 'info', |
| 2145 | + 'password': '', |
| 2146 | + 'port': 4242, |
| 2147 | + } |
| 2148 | + slave_relation = make_relation({ |
| 2149 | + 'hostname': '4.3.2.1', |
| 2150 | + 'password': 'sercret!', |
| 2151 | + 'port': 90, |
| 2152 | + }) |
| 2153 | + callback = serviceutils.write_config_file( |
| 2154 | + config, slave_relation=slave_relation) |
| 2155 | + with self.patch_all() as mocks: |
| 2156 | + callback('foo') |
| 2157 | + mocks.write.assert_called_once_with({ |
| 2158 | + 'bind': '1.2.3.4', |
| 2159 | + 'logfile': '/path/to/logs', |
| 2160 | + 'loglevel': 'info', |
| 2161 | + 'masterauth': 'sercret!', |
| 2162 | + 'port': 4242, |
| 2163 | + 'slaveof': '4.3.2.1 90' |
| 2164 | + }, configfile.REDIS_CONF) |
| 2165 | + mocks.unit_get.assert_called_once_with('public-address') |
| 2166 | + self.assertFalse(mocks.service_restart.called) |

TODO: open/close ports, more config options.