Merge lp:~james-page/charms/precise/ceph-osd/charm-helpers into lp:~charmers/charms/precise/ceph-osd/trunk

Proposed by James Page
Status: Merged
Merged at revision: 14
Proposed branch: lp:~james-page/charms/precise/ceph-osd/charm-helpers
Merge into: lp:~charmers/charms/precise/ceph-osd/trunk
Diff against target: 2097 lines (+1485/-301)
11 files modified
Makefile (+8/-0)
charm-helpers-sync.yaml (+7/-0)
hooks/ceph.py (+126/-26)
hooks/charmhelpers/contrib/storage/linux/utils.py (+25/-0)
hooks/charmhelpers/core/hookenv.py (+334/-0)
hooks/charmhelpers/core/host.py (+273/-0)
hooks/charmhelpers/fetch/__init__.py (+152/-0)
hooks/charmhelpers/fetch/archiveurl.py (+43/-0)
hooks/hooks.py (+85/-111)
hooks/utils.py (+18/-164)
icon.svg (+414/-0)
To merge this branch: bzr merge lp:~james-page/charms/precise/ceph-osd/charm-helpers
Reviewer Review Type Date Requested Status
Mark Mims (community) Approve
Review via email: mp+173246@code.launchpad.net

Description of the change

Refactoring to support use with charm-helpers

Significant rework to support use of charm-helpers rather than its own
utils.py of fun.

Also fixes a couple of issues with newer versions of ceph which no longer automatically zap disks.

To post a comment you must log in.
29. By James Page

Fixup dodgy disk detection

Revision history for this message
Mark Mims (mark-mims) wrote :

Awesome... great direction.

Two requests for the future direction of this charm:

- consider refactoring $CHARM_DIR/hooks/hooks.py into $CHARM_DIR/lib/ceph_tools with accompanying unit-type $CHARM_DIR/lib/ceph_tools/tests where possible

- please think up some decent integration tests and add them into $CHARM_DIR/tests

Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Makefile'
2--- Makefile 1970-01-01 00:00:00 +0000
3+++ Makefile 2013-07-08 08:34:32 +0000
4@@ -0,0 +1,8 @@
5+#!/usr/bin/make
6+
7+lint:
8+ @flake8 --exclude hooks/charmhelpers hooks
9+ @charm proof || true
10+
11+sync:
12+ @charm-helper-sync -c charm-helpers-sync.yaml
13
14=== added file 'charm-helpers-sync.yaml'
15--- charm-helpers-sync.yaml 1970-01-01 00:00:00 +0000
16+++ charm-helpers-sync.yaml 2013-07-08 08:34:32 +0000
17@@ -0,0 +1,7 @@
18+branch: lp:charm-helpers
19+destination: hooks/charmhelpers
20+include:
21+ - core
22+ - fetch
23+ - contrib.storage.linux:
24+ - utils
25
26=== modified file 'hooks/ceph.py'
27--- hooks/ceph.py 2012-12-18 10:26:09 +0000
28+++ hooks/ceph.py 2013-07-08 08:34:32 +0000
29@@ -10,23 +10,36 @@
30 import json
31 import subprocess
32 import time
33-import utils
34 import os
35 import apt_pkg as apt
36+from charmhelpers.core.host import (
37+ mkdir,
38+ service_restart,
39+ log
40+)
41+from charmhelpers.contrib.storage.linux.utils import (
42+ zap_disk,
43+ is_block_device
44+)
45+from utils import (
46+ get_unit_hostname
47+)
48
49 LEADER = 'leader'
50 PEON = 'peon'
51 QUORUM = [LEADER, PEON]
52
53+PACKAGES = ['ceph', 'gdisk', 'ntp', 'btrfs-tools', 'python-ceph', 'xfsprogs']
54+
55
56 def is_quorum():
57- asok = "/var/run/ceph/ceph-mon.{}.asok".format(utils.get_unit_hostname())
58+ asok = "/var/run/ceph/ceph-mon.{}.asok".format(get_unit_hostname())
59 cmd = [
60 "ceph",
61 "--admin-daemon",
62 asok,
63 "mon_status"
64- ]
65+ ]
66 if os.path.exists(asok):
67 try:
68 result = json.loads(subprocess.check_output(cmd))
69@@ -44,13 +57,13 @@
70
71
72 def is_leader():
73- asok = "/var/run/ceph/ceph-mon.{}.asok".format(utils.get_unit_hostname())
74+ asok = "/var/run/ceph/ceph-mon.{}.asok".format(get_unit_hostname())
75 cmd = [
76 "ceph",
77 "--admin-daemon",
78 asok,
79 "mon_status"
80- ]
81+ ]
82 if os.path.exists(asok):
83 try:
84 result = json.loads(subprocess.check_output(cmd))
85@@ -73,14 +86,14 @@
86
87
88 def add_bootstrap_hint(peer):
89- asok = "/var/run/ceph/ceph-mon.{}.asok".format(utils.get_unit_hostname())
90+ asok = "/var/run/ceph/ceph-mon.{}.asok".format(get_unit_hostname())
91 cmd = [
92 "ceph",
93 "--admin-daemon",
94 asok,
95 "add_bootstrap_peer_hint",
96 peer
97- ]
98+ ]
99 if os.path.exists(asok):
100 # Ignore any errors for this call
101 subprocess.call(cmd)
102@@ -89,7 +102,7 @@
103 'xfs',
104 'ext4',
105 'btrfs'
106- ]
107+]
108
109
110 def is_osd_disk(dev):
111@@ -99,7 +112,7 @@
112 for line in info:
113 if line.startswith(
114 'Partition GUID code: 4FBD7E29-9D25-41B8-AFD0-062C0CEFF05D'
115- ):
116+ ):
117 return True
118 except subprocess.CalledProcessError:
119 pass
120@@ -110,16 +123,11 @@
121 cmd = [
122 'udevadm', 'trigger',
123 '--subsystem-match=block', '--action=add'
124- ]
125+ ]
126
127 subprocess.call(cmd)
128
129
130-def zap_disk(dev):
131- cmd = ['sgdisk', '--zap-all', dev]
132- subprocess.check_call(cmd)
133-
134-
135 _bootstrap_keyring = "/var/lib/ceph/bootstrap-osd/ceph.keyring"
136
137
138@@ -140,7 +148,7 @@
139 '--create-keyring',
140 '--name=client.bootstrap-osd',
141 '--add-key={}'.format(key)
142- ]
143+ ]
144 subprocess.check_call(cmd)
145
146 # OSD caps taken from ceph-create-keys
147@@ -148,10 +156,10 @@
148 'mon': [
149 'allow command osd create ...',
150 'allow command osd crush set ...',
151- r'allow command auth add * osd allow\ * mon allow\ rwx',
152+ r'allow command auth add * osd allow\ * mon allow\ rwx',
153 'allow command mon getmap'
154- ]
155- }
156+ ]
157+}
158
159
160 def get_osd_bootstrap_key():
161@@ -169,14 +177,14 @@
162 '--create-keyring',
163 '--name=client.radosgw.gateway',
164 '--add-key={}'.format(key)
165- ]
166+ ]
167 subprocess.check_call(cmd)
168
169 # OSD caps taken from ceph-create-keys
170 _radosgw_caps = {
171 'mon': ['allow r'],
172 'osd': ['allow rwx']
173- }
174+}
175
176
177 def get_radosgw_key():
178@@ -186,7 +194,7 @@
179 _default_caps = {
180 'mon': ['allow r'],
181 'osd': ['allow rwx']
182- }
183+}
184
185
186 def get_named_key(name, caps=None):
187@@ -196,16 +204,16 @@
188 '--name', 'mon.',
189 '--keyring',
190 '/var/lib/ceph/mon/ceph-{}/keyring'.format(
191- utils.get_unit_hostname()
192- ),
193+ get_unit_hostname()
194+ ),
195 'auth', 'get-or-create', 'client.{}'.format(name),
196- ]
197+ ]
198 # Add capabilities
199 for subsystem, subcaps in caps.iteritems():
200 cmd.extend([
201 subsystem,
202 '; '.join(subcaps),
203- ])
204+ ])
205 output = subprocess.check_output(cmd).strip() # IGNORE:E1103
206 # get-or-create appears to have different output depending
207 # on whether its 'get' or 'create'
208@@ -221,6 +229,42 @@
209 return key
210
211
212+def bootstrap_monitor_cluster(secret):
213+ hostname = get_unit_hostname()
214+ path = '/var/lib/ceph/mon/ceph-{}'.format(hostname)
215+ done = '{}/done'.format(path)
216+ upstart = '{}/upstart'.format(path)
217+ keyring = '/var/lib/ceph/tmp/{}.mon.keyring'.format(hostname)
218+
219+ if os.path.exists(done):
220+ log('bootstrap_monitor_cluster: mon already initialized.')
221+ else:
222+ # Ceph >= 0.61.3 needs this for ceph-mon fs creation
223+ mkdir('/var/run/ceph', perms=0755)
224+ mkdir(path)
225+ # end changes for Ceph >= 0.61.3
226+ try:
227+ subprocess.check_call(['ceph-authtool', keyring,
228+ '--create-keyring', '--name=mon.',
229+ '--add-key={}'.format(secret),
230+ '--cap', 'mon', 'allow *'])
231+
232+ subprocess.check_call(['ceph-mon', '--mkfs',
233+ '-i', hostname,
234+ '--keyring', keyring])
235+
236+ with open(done, 'w'):
237+ pass
238+ with open(upstart, 'w'):
239+ pass
240+
241+ service_restart('ceph-mon-all')
242+ except:
243+ raise
244+ finally:
245+ os.unlink(keyring)
246+
247+
248 def get_ceph_version():
249 apt.init()
250 cache = apt.Cache()
251@@ -233,3 +277,59 @@
252
253 def version_compare(a, b):
254 return apt.version_compare(a, b)
255+
256+
257+def update_monfs():
258+ hostname = get_unit_hostname()
259+ monfs = '/var/lib/ceph/mon/ceph-{}'.format(hostname)
260+ upstart = '{}/upstart'.format(monfs)
261+ if os.path.exists(monfs) and not os.path.exists(upstart):
262+ # Mark mon as managed by upstart so that
263+ # it gets start correctly on reboots
264+ with open(upstart, 'w'):
265+ pass
266+
267+
268+def osdize(dev, osd_format, osd_journal, reformat_osd=False):
269+ if not os.path.exists(dev):
270+ log('Path {} does not exist - bailing'.format(dev))
271+ return
272+
273+ if not is_block_device(dev):
274+ log('Path {} is not a block device - bailing'.format(dev))
275+ return
276+
277+ if (is_osd_disk(dev) and not reformat_osd):
278+ log('Looks like {} is already an OSD, skipping.'.format(dev))
279+ return
280+
281+ if device_mounted(dev):
282+ log('Looks like {} is in use, skipping.'.format(dev))
283+ return
284+
285+ cmd = ['ceph-disk-prepare']
286+ # Later versions of ceph support more options
287+ if get_ceph_version() >= "0.48.3":
288+ if osd_format:
289+ cmd.append('--fs-type')
290+ cmd.append(osd_format)
291+ cmd.append(dev)
292+ if osd_journal and os.path.exists(osd_journal):
293+ cmd.append(osd_journal)
294+ else:
295+ # Just provide the device - no other options
296+ # for older versions of ceph
297+ cmd.append(dev)
298+
299+ if reformat_osd:
300+ zap_disk(dev)
301+
302+ subprocess.check_call(cmd)
303+
304+
305+def device_mounted(dev):
306+ return subprocess.call(['grep', '-wqs', dev + '1', '/proc/mounts']) == 0
307+
308+
309+def filesystem_mounted(fs):
310+ return subprocess.call(['grep', '-wqs', fs, '/proc/mounts']) == 0
311
312=== added directory 'hooks/charmhelpers'
313=== added file 'hooks/charmhelpers/__init__.py'
314=== added directory 'hooks/charmhelpers/contrib'
315=== added file 'hooks/charmhelpers/contrib/__init__.py'
316=== added directory 'hooks/charmhelpers/contrib/storage'
317=== added file 'hooks/charmhelpers/contrib/storage/__init__.py'
318=== added directory 'hooks/charmhelpers/contrib/storage/linux'
319=== added file 'hooks/charmhelpers/contrib/storage/linux/__init__.py'
320=== added file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
321--- hooks/charmhelpers/contrib/storage/linux/utils.py 1970-01-01 00:00:00 +0000
322+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2013-07-08 08:34:32 +0000
323@@ -0,0 +1,25 @@
324+from os import stat
325+from stat import S_ISBLK
326+
327+from subprocess import (
328+ check_call
329+)
330+
331+
332+def is_block_device(path):
333+ '''
334+ Confirm device at path is a valid block device node.
335+
336+ :returns: boolean: True if path is a block device, False if not.
337+ '''
338+ return S_ISBLK(stat(path).st_mode)
339+
340+
341+def zap_disk(block_device):
342+ '''
343+ Clear a block device of partition table. Relies on sgdisk, which is
344+ installed as pat of the 'gdisk' package in Ubuntu.
345+
346+ :param block_device: str: Full path of block device to clean.
347+ '''
348+ check_call(['sgdisk', '--zap-all', block_device])
349
350=== added directory 'hooks/charmhelpers/core'
351=== added file 'hooks/charmhelpers/core/__init__.py'
352=== added file 'hooks/charmhelpers/core/hookenv.py'
353--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
354+++ hooks/charmhelpers/core/hookenv.py 2013-07-08 08:34:32 +0000
355@@ -0,0 +1,334 @@
356+"Interactions with the Juju environment"
357+# Copyright 2013 Canonical Ltd.
358+#
359+# Authors:
360+# Charm Helpers Developers <juju@lists.ubuntu.com>
361+
362+import os
363+import json
364+import yaml
365+import subprocess
366+import UserDict
367+
368+CRITICAL = "CRITICAL"
369+ERROR = "ERROR"
370+WARNING = "WARNING"
371+INFO = "INFO"
372+DEBUG = "DEBUG"
373+MARKER = object()
374+
375+cache = {}
376+
377+
378+def cached(func):
379+ ''' Cache return values for multiple executions of func + args
380+
381+ For example:
382+
383+ @cached
384+ def unit_get(attribute):
385+ pass
386+
387+ unit_get('test')
388+
389+ will cache the result of unit_get + 'test' for future calls.
390+ '''
391+ def wrapper(*args, **kwargs):
392+ global cache
393+ key = str((func, args, kwargs))
394+ try:
395+ return cache[key]
396+ except KeyError:
397+ res = func(*args, **kwargs)
398+ cache[key] = res
399+ return res
400+ return wrapper
401+
402+
403+def flush(key):
404+ ''' Flushes any entries from function cache where the
405+ key is found in the function+args '''
406+ flush_list = []
407+ for item in cache:
408+ if key in item:
409+ flush_list.append(item)
410+ for item in flush_list:
411+ del cache[item]
412+
413+
414+def log(message, level=None):
415+ "Write a message to the juju log"
416+ command = ['juju-log']
417+ if level:
418+ command += ['-l', level]
419+ command += [message]
420+ subprocess.call(command)
421+
422+
423+class Serializable(UserDict.IterableUserDict):
424+ "Wrapper, an object that can be serialized to yaml or json"
425+
426+ def __init__(self, obj):
427+ # wrap the object
428+ UserDict.IterableUserDict.__init__(self)
429+ self.data = obj
430+
431+ def __getattr__(self, attr):
432+ # See if this object has attribute.
433+ if attr in ("json", "yaml", "data"):
434+ return self.__dict__[attr]
435+ # Check for attribute in wrapped object.
436+ got = getattr(self.data, attr, MARKER)
437+ if got is not MARKER:
438+ return got
439+ # Proxy to the wrapped object via dict interface.
440+ try:
441+ return self.data[attr]
442+ except KeyError:
443+ raise AttributeError(attr)
444+
445+ def __getstate__(self):
446+ # Pickle as a standard dictionary.
447+ return self.data
448+
449+ def __setstate__(self, state):
450+ # Unpickle into our wrapper.
451+ self.data = state
452+
453+ def json(self):
454+ "Serialize the object to json"
455+ return json.dumps(self.data)
456+
457+ def yaml(self):
458+ "Serialize the object to yaml"
459+ return yaml.dump(self.data)
460+
461+
462+def execution_environment():
463+ """A convenient bundling of the current execution context"""
464+ context = {}
465+ context['conf'] = config()
466+ if relation_id():
467+ context['reltype'] = relation_type()
468+ context['relid'] = relation_id()
469+ context['rel'] = relation_get()
470+ context['unit'] = local_unit()
471+ context['rels'] = relations()
472+ context['env'] = os.environ
473+ return context
474+
475+
476+def in_relation_hook():
477+ "Determine whether we're running in a relation hook"
478+ return 'JUJU_RELATION' in os.environ
479+
480+
481+def relation_type():
482+ "The scope for the current relation hook"
483+ return os.environ.get('JUJU_RELATION', None)
484+
485+
486+def relation_id():
487+ "The relation ID for the current relation hook"
488+ return os.environ.get('JUJU_RELATION_ID', None)
489+
490+
491+def local_unit():
492+ "Local unit ID"
493+ return os.environ['JUJU_UNIT_NAME']
494+
495+
496+def remote_unit():
497+ "The remote unit for the current relation hook"
498+ return os.environ['JUJU_REMOTE_UNIT']
499+
500+
501+@cached
502+def config(scope=None):
503+ "Juju charm configuration"
504+ config_cmd_line = ['config-get']
505+ if scope is not None:
506+ config_cmd_line.append(scope)
507+ config_cmd_line.append('--format=json')
508+ try:
509+ return json.loads(subprocess.check_output(config_cmd_line))
510+ except ValueError:
511+ return None
512+
513+
514+@cached
515+def relation_get(attribute=None, unit=None, rid=None):
516+ _args = ['relation-get', '--format=json']
517+ if rid:
518+ _args.append('-r')
519+ _args.append(rid)
520+ _args.append(attribute or '-')
521+ if unit:
522+ _args.append(unit)
523+ try:
524+ return json.loads(subprocess.check_output(_args))
525+ except ValueError:
526+ return None
527+
528+
529+def relation_set(relation_id=None, relation_settings={}, **kwargs):
530+ relation_cmd_line = ['relation-set']
531+ if relation_id is not None:
532+ relation_cmd_line.extend(('-r', relation_id))
533+ for k, v in (relation_settings.items() + kwargs.items()):
534+ if v is None:
535+ relation_cmd_line.append('{}='.format(k))
536+ else:
537+ relation_cmd_line.append('{}={}'.format(k, v))
538+ subprocess.check_call(relation_cmd_line)
539+ # Flush cache of any relation-gets for local unit
540+ flush(local_unit())
541+
542+
543+@cached
544+def relation_ids(reltype=None):
545+ "A list of relation_ids"
546+ reltype = reltype or relation_type()
547+ relid_cmd_line = ['relation-ids', '--format=json']
548+ if reltype is not None:
549+ relid_cmd_line.append(reltype)
550+ return json.loads(subprocess.check_output(relid_cmd_line))
551+ return []
552+
553+
554+@cached
555+def related_units(relid=None):
556+ "A list of related units"
557+ relid = relid or relation_id()
558+ units_cmd_line = ['relation-list', '--format=json']
559+ if relid is not None:
560+ units_cmd_line.extend(('-r', relid))
561+ return json.loads(subprocess.check_output(units_cmd_line))
562+
563+
564+@cached
565+def relation_for_unit(unit=None, rid=None):
566+ "Get the json represenation of a unit's relation"
567+ unit = unit or remote_unit()
568+ relation = relation_get(unit=unit, rid=rid)
569+ for key in relation:
570+ if key.endswith('-list'):
571+ relation[key] = relation[key].split()
572+ relation['__unit__'] = unit
573+ return relation
574+
575+
576+@cached
577+def relations_for_id(relid=None):
578+ "Get relations of a specific relation ID"
579+ relation_data = []
580+ relid = relid or relation_ids()
581+ for unit in related_units(relid):
582+ unit_data = relation_for_unit(unit, relid)
583+ unit_data['__relid__'] = relid
584+ relation_data.append(unit_data)
585+ return relation_data
586+
587+
588+@cached
589+def relations_of_type(reltype=None):
590+ "Get relations of a specific type"
591+ relation_data = []
592+ reltype = reltype or relation_type()
593+ for relid in relation_ids(reltype):
594+ for relation in relations_for_id(relid):
595+ relation['__relid__'] = relid
596+ relation_data.append(relation)
597+ return relation_data
598+
599+
600+@cached
601+def relation_types():
602+ "Get a list of relation types supported by this charm"
603+ charmdir = os.environ.get('CHARM_DIR', '')
604+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
605+ md = yaml.safe_load(mdf)
606+ rel_types = []
607+ for key in ('provides', 'requires', 'peers'):
608+ section = md.get(key)
609+ if section:
610+ rel_types.extend(section.keys())
611+ mdf.close()
612+ return rel_types
613+
614+
615+@cached
616+def relations():
617+ rels = {}
618+ for reltype in relation_types():
619+ relids = {}
620+ for relid in relation_ids(reltype):
621+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
622+ for unit in related_units(relid):
623+ reldata = relation_get(unit=unit, rid=relid)
624+ units[unit] = reldata
625+ relids[relid] = units
626+ rels[reltype] = relids
627+ return rels
628+
629+
630+def open_port(port, protocol="TCP"):
631+ "Open a service network port"
632+ _args = ['open-port']
633+ _args.append('{}/{}'.format(port, protocol))
634+ subprocess.check_call(_args)
635+
636+
637+def close_port(port, protocol="TCP"):
638+ "Close a service network port"
639+ _args = ['close-port']
640+ _args.append('{}/{}'.format(port, protocol))
641+ subprocess.check_call(_args)
642+
643+
644+@cached
645+def unit_get(attribute):
646+ _args = ['unit-get', '--format=json', attribute]
647+ try:
648+ return json.loads(subprocess.check_output(_args))
649+ except ValueError:
650+ return None
651+
652+
653+def unit_private_ip():
654+ return unit_get('private-address')
655+
656+
657+class UnregisteredHookError(Exception):
658+ pass
659+
660+
661+class Hooks(object):
662+ def __init__(self):
663+ super(Hooks, self).__init__()
664+ self._hooks = {}
665+
666+ def register(self, name, function):
667+ self._hooks[name] = function
668+
669+ def execute(self, args):
670+ hook_name = os.path.basename(args[0])
671+ if hook_name in self._hooks:
672+ self._hooks[hook_name]()
673+ else:
674+ raise UnregisteredHookError(hook_name)
675+
676+ def hook(self, *hook_names):
677+ def wrapper(decorated):
678+ for hook_name in hook_names:
679+ self.register(hook_name, decorated)
680+ else:
681+ self.register(decorated.__name__, decorated)
682+ if '_' in decorated.__name__:
683+ self.register(
684+ decorated.__name__.replace('_', '-'), decorated)
685+ return decorated
686+ return wrapper
687+
688+def charm_dir():
689+ return os.environ.get('CHARM_DIR')
690
691=== added file 'hooks/charmhelpers/core/host.py'
692--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
693+++ hooks/charmhelpers/core/host.py 2013-07-08 08:34:32 +0000
694@@ -0,0 +1,273 @@
695+"""Tools for working with the host system"""
696+# Copyright 2012 Canonical Ltd.
697+#
698+# Authors:
699+# Nick Moffitt <nick.moffitt@canonical.com>
700+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
701+
702+import apt_pkg
703+import os
704+import pwd
705+import grp
706+import subprocess
707+import hashlib
708+
709+from collections import OrderedDict
710+
711+from hookenv import log, execution_environment
712+
713+
714+def service_start(service_name):
715+ service('start', service_name)
716+
717+
718+def service_stop(service_name):
719+ service('stop', service_name)
720+
721+
722+def service_restart(service_name):
723+ service('restart', service_name)
724+
725+
726+def service_reload(service_name, restart_on_failure=False):
727+ if not service('reload', service_name) and restart_on_failure:
728+ service('restart', service_name)
729+
730+
731+def service(action, service_name):
732+ cmd = ['service', service_name, action]
733+ return subprocess.call(cmd) == 0
734+
735+
736+def adduser(username, password=None, shell='/bin/bash', system_user=False):
737+ """Add a user"""
738+ try:
739+ user_info = pwd.getpwnam(username)
740+ log('user {0} already exists!'.format(username))
741+ except KeyError:
742+ log('creating user {0}'.format(username))
743+ cmd = ['useradd']
744+ if system_user or password is None:
745+ cmd.append('--system')
746+ else:
747+ cmd.extend([
748+ '--create-home',
749+ '--shell', shell,
750+ '--password', password,
751+ ])
752+ cmd.append(username)
753+ subprocess.check_call(cmd)
754+ user_info = pwd.getpwnam(username)
755+ return user_info
756+
757+
758+def add_user_to_group(username, group):
759+ """Add a user to a group"""
760+ cmd = [
761+ 'gpasswd', '-a',
762+ username,
763+ group
764+ ]
765+ log("Adding user {} to group {}".format(username, group))
766+ subprocess.check_call(cmd)
767+
768+
769+def rsync(from_path, to_path, flags='-r', options=None):
770+ """Replicate the contents of a path"""
771+ context = execution_environment()
772+ options = options or ['--delete', '--executability']
773+ cmd = ['/usr/bin/rsync', flags]
774+ cmd.extend(options)
775+ cmd.append(from_path.format(**context))
776+ cmd.append(to_path.format(**context))
777+ log(" ".join(cmd))
778+ return subprocess.check_output(cmd).strip()
779+
780+
781+def symlink(source, destination):
782+ """Create a symbolic link"""
783+ context = execution_environment()
784+ log("Symlinking {} as {}".format(source, destination))
785+ cmd = [
786+ 'ln',
787+ '-sf',
788+ source.format(**context),
789+ destination.format(**context)
790+ ]
791+ subprocess.check_call(cmd)
792+
793+
794+def mkdir(path, owner='root', group='root', perms=0555, force=False):
795+ """Create a directory"""
796+ context = execution_environment()
797+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
798+ perms))
799+ uid = pwd.getpwnam(owner.format(**context)).pw_uid
800+ gid = grp.getgrnam(group.format(**context)).gr_gid
801+ realpath = os.path.abspath(path)
802+ if os.path.exists(realpath):
803+ if force and not os.path.isdir(realpath):
804+ log("Removing non-directory file {} prior to mkdir()".format(path))
805+ os.unlink(realpath)
806+ else:
807+ os.makedirs(realpath, perms)
808+ os.chown(realpath, uid, gid)
809+
810+
811+def write_file(path, fmtstr, owner='root', group='root', perms=0444, **kwargs):
812+ """Create or overwrite a file with the contents of a string"""
813+ context = execution_environment()
814+ context.update(kwargs)
815+ log("Writing file {} {}:{} {:o}".format(path, owner, group,
816+ perms))
817+ uid = pwd.getpwnam(owner.format(**context)).pw_uid
818+ gid = grp.getgrnam(group.format(**context)).gr_gid
819+ with open(path.format(**context), 'w') as target:
820+ os.fchown(target.fileno(), uid, gid)
821+ os.fchmod(target.fileno(), perms)
822+ target.write(fmtstr.format(**context))
823+
824+
825+def render_template_file(source, destination, **kwargs):
826+ """Create or overwrite a file using a template"""
827+ log("Rendering template {} for {}".format(source,
828+ destination))
829+ context = execution_environment()
830+ with open(source.format(**context), 'r') as template:
831+ write_file(destination.format(**context), template.read(),
832+ **kwargs)
833+
834+
835+def filter_installed_packages(packages):
836+ """Returns a list of packages that require installation"""
837+ apt_pkg.init()
838+ cache = apt_pkg.Cache()
839+ _pkgs = []
840+ for package in packages:
841+ try:
842+ p = cache[package]
843+ p.current_ver or _pkgs.append(package)
844+ except KeyError:
845+ log('Package {} has no installation candidate.'.format(package),
846+ level='WARNING')
847+ _pkgs.append(package)
848+ return _pkgs
849+
850+
851+def apt_install(packages, options=None, fatal=False):
852+ """Install one or more packages"""
853+ options = options or []
854+ cmd = ['apt-get', '-y']
855+ cmd.extend(options)
856+ cmd.append('install')
857+ if isinstance(packages, basestring):
858+ cmd.append(packages)
859+ else:
860+ cmd.extend(packages)
861+ log("Installing {} with options: {}".format(packages,
862+ options))
863+ if fatal:
864+ subprocess.check_call(cmd)
865+ else:
866+ subprocess.call(cmd)
867+
868+
869+def apt_update(fatal=False):
870+ """Update local apt cache"""
871+ cmd = ['apt-get', 'update']
872+ if fatal:
873+ subprocess.check_call(cmd)
874+ else:
875+ subprocess.call(cmd)
876+
877+
878+def mount(device, mountpoint, options=None, persist=False):
879+ '''Mount a filesystem'''
880+ cmd_args = ['mount']
881+ if options is not None:
882+ cmd_args.extend(['-o', options])
883+ cmd_args.extend([device, mountpoint])
884+ try:
885+ subprocess.check_output(cmd_args)
886+ except subprocess.CalledProcessError, e:
887+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
888+ return False
889+ if persist:
890+ # TODO: update fstab
891+ pass
892+ return True
893+
894+
895+def umount(mountpoint, persist=False):
896+ '''Unmount a filesystem'''
897+ cmd_args = ['umount', mountpoint]
898+ try:
899+ subprocess.check_output(cmd_args)
900+ except subprocess.CalledProcessError, e:
901+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
902+ return False
903+ if persist:
904+ # TODO: update fstab
905+ pass
906+ return True
907+
908+
909+def mounts():
910+ '''List of all mounted volumes as [[mountpoint,device],[...]]'''
911+ with open('/proc/mounts') as f:
912+ # [['/mount/point','/dev/path'],[...]]
913+ system_mounts = [m[1::-1] for m in [l.strip().split()
914+ for l in f.readlines()]]
915+ return system_mounts
916+
917+
918+def file_hash(path):
919+ ''' Generate a md5 hash of the contents of 'path' or None if not found '''
920+ if os.path.exists(path):
921+ h = hashlib.md5()
922+ with open(path, 'r') as source:
923+ h.update(source.read()) # IGNORE:E1101 - it does have update
924+ return h.hexdigest()
925+ else:
926+ return None
927+
928+
929+def restart_on_change(restart_map):
930+ ''' Restart services based on configuration files changing
931+
932+ This function is used a decorator, for example
933+
934+ @restart_on_change({
935+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
936+ })
937+ def ceph_client_changed():
938+ ...
939+
940+ In this example, the cinder-api and cinder-volume services
941+ would be restarted if /etc/ceph/ceph.conf is changed by the
942+ ceph_client_changed function.
943+ '''
944+ def wrap(f):
945+ def wrapped_f(*args):
946+ checksums = {}
947+ for path in restart_map:
948+ checksums[path] = file_hash(path)
949+ f(*args)
950+ restarts = []
951+ for path in restart_map:
952+ if checksums[path] != file_hash(path):
953+ restarts += restart_map[path]
954+ for service_name in list(OrderedDict.fromkeys(restarts)):
955+ service('restart', service_name)
956+ return wrapped_f
957+ return wrap
958+
959+
960+def lsb_release():
961+ '''Return /etc/lsb-release in a dict'''
962+ d = {}
963+ with open('/etc/lsb-release', 'r') as lsb:
964+ for l in lsb:
965+ k, v = l.split('=')
966+ d[k.strip()] = v.strip()
967+ return d
968
969=== added directory 'hooks/charmhelpers/fetch'
970=== added file 'hooks/charmhelpers/fetch/__init__.py'
971--- hooks/charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000
972+++ hooks/charmhelpers/fetch/__init__.py 2013-07-08 08:34:32 +0000
973@@ -0,0 +1,152 @@
974+import importlib
975+from yaml import safe_load
976+from charmhelpers.core.host import (
977+ apt_install,
978+ apt_update,
979+ filter_installed_packages,
980+ lsb_release
981+)
982+from urlparse import (
983+ urlparse,
984+ urlunparse,
985+)
986+import subprocess
987+from charmhelpers.core.hookenv import (
988+ config,
989+ log,
990+)
991+
992+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
993+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
994+"""
995+PROPOSED_POCKET = """# Proposed
996+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
997+"""
998+
999+
1000+def add_source(source, key=None):
1001+ if ((source.startswith('ppa:') or
1002+ source.startswith('http:'))):
1003+ subprocess.check_call(['add-apt-repository', source])
1004+ elif source.startswith('cloud:'):
1005+ apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
1006+ fatal=True)
1007+ pocket = source.split(':')[-1]
1008+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
1009+ apt.write(CLOUD_ARCHIVE.format(pocket))
1010+ elif source == 'proposed':
1011+ release = lsb_release()['DISTRIB_CODENAME']
1012+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
1013+ apt.write(PROPOSED_POCKET.format(release))
1014+ if key:
1015+ subprocess.check_call(['apt-key', 'import', key])
1016+
1017+
1018+class SourceConfigError(Exception):
1019+ pass
1020+
1021+
1022+def configure_sources(update=False,
1023+ sources_var='install_sources',
1024+ keys_var='install_keys'):
1025+ """
1026+ Configure multiple sources from charm configuration
1027+
1028+ Example config:
1029+ install_sources:
1030+ - "ppa:foo"
1031+ - "http://example.com/repo precise main"
1032+ install_keys:
1033+ - null
1034+ - "a1b2c3d4"
1035+
1036+ Note that 'null' (a.k.a. None) should not be quoted.
1037+ """
1038+ sources = safe_load(config(sources_var))
1039+ keys = safe_load(config(keys_var))
1040+ if isinstance(sources, basestring) and isinstance(keys, basestring):
1041+ add_source(sources, keys)
1042+ else:
1043+ if not len(sources) == len(keys):
1044+ msg = 'Install sources and keys lists are different lengths'
1045+ raise SourceConfigError(msg)
1046+ for src_num in range(len(sources)):
1047+ add_source(sources[src_num], keys[src_num])
1048+ if update:
1049+ apt_update(fatal=True)
1050+
1051+# The order of this list is very important. Handlers should be listed in from
1052+# least- to most-specific URL matching.
1053+FETCH_HANDLERS = (
1054+ 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
1055+)
1056+
1057+
1058+class UnhandledSource(Exception):
1059+ pass
1060+
1061+
1062+def install_remote(source):
1063+ """
1064+ Install a file tree from a remote source
1065+
1066+ The specified source should be a url of the form:
1067+ scheme://[host]/path[#[option=value][&...]]
1068+
1069+ Schemes supported are based on this modules submodules
1070+ Options supported are submodule-specific"""
1071+ # We ONLY check for True here because can_handle may return a string
1072+ # explaining why it can't handle a given source.
1073+ handlers = [h for h in plugins() if h.can_handle(source) is True]
1074+ for handler in handlers:
1075+ try:
1076+ installed_to = handler.install(source)
1077+ except UnhandledSource:
1078+ pass
1079+ if not installed_to:
1080+ raise UnhandledSource("No handler found for source {}".format(source))
1081+ return installed_to
1082+
1083+
1084+def install_from_config(config_var_name):
1085+ charm_config = config()
1086+ source = charm_config[config_var_name]
1087+ return install_remote(source)
1088+
1089+
1090+class BaseFetchHandler(object):
1091+ """Base class for FetchHandler implementations in fetch plugins"""
1092+ def can_handle(self, source):
1093+ """Returns True if the source can be handled. Otherwise returns
1094+ a string explaining why it cannot"""
1095+ return "Wrong source type"
1096+
1097+ def install(self, source):
1098+ """Try to download and unpack the source. Return the path to the
1099+ unpacked files or raise UnhandledSource."""
1100+ raise UnhandledSource("Wrong source type {}".format(source))
1101+
1102+ def parse_url(self, url):
1103+ return urlparse(url)
1104+
1105+ def base_url(self, url):
1106+ """Return url without querystring or fragment"""
1107+ parts = list(self.parse_url(url))
1108+ parts[4:] = ['' for i in parts[4:]]
1109+ return urlunparse(parts)
1110+
1111+
1112+def plugins(fetch_handlers=None):
1113+ if not fetch_handlers:
1114+ fetch_handlers = FETCH_HANDLERS
1115+ plugin_list = []
1116+ for handler_name in fetch_handlers:
1117+ package, classname = handler_name.rsplit('.', 1)
1118+ try:
1119+ handler_class = getattr(importlib.import_module(package), classname)
1120+ plugin_list.append(handler_class())
1121+ except (ImportError, AttributeError):
1122+ # Skip missing plugins so that they can be ommitted from
1123+ # installation if desired
1124+ log("FetchHandler {} not found, skipping plugin".format(handler_name))
1125+ return plugin_list
1126
1127=== added file 'hooks/charmhelpers/fetch/archiveurl.py'
1128--- hooks/charmhelpers/fetch/archiveurl.py 1970-01-01 00:00:00 +0000
1129+++ hooks/charmhelpers/fetch/archiveurl.py 2013-07-08 08:34:32 +0000
1130@@ -0,0 +1,43 @@
1131+import os
1132+import urllib2
1133+from charmhelpers.fetch import (
1134+ BaseFetchHandler,
1135+ UnhandledSource
1136+)
1137+from charmhelpers.payload.archive import (
1138+ get_archive_handler,
1139+ extract,
1140+)
1141+
1142+
1143+class ArchiveUrlFetchHandler(BaseFetchHandler):
1144+ """Handler for archives via generic URLs"""
1145+ def can_handle(self, source):
1146+ url_parts = self.parse_url(source)
1147+ if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
1148+ return "Wrong source type"
1149+ if get_archive_handler(self.base_url(source)):
1150+ return True
1151+ return False
1152+
1153+ def download(self, source, dest):
1154+ # propogate all exceptions
1155+ # URLError, OSError, etc
1156+ response = urllib2.urlopen(source)
1157+ with open(dest, 'w') as dest_file:
1158+ dest_file.write(response.read())
1159+
1160+ def install(self, source):
1161+ url_parts = self.parse_url(source)
1162+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
1163+ dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
1164+ try:
1165+ self.download(source, dld_file)
1166+ except urllib2.URLError as e:
1167+ return UnhandledSource(e.reason)
1168+ except OSError as e:
1169+ return UnhandledSource(e.strerror)
1170+ finally:
1171+ if os.path.isfile(dld_file):
1172+ os.unlink(dld_file)
1173+ return extract(dld_file)
1174
1175=== modified file 'hooks/hooks.py'
1176--- hooks/hooks.py 2013-06-10 13:48:23 +0000
1177+++ hooks/hooks.py 2013-07-08 08:34:32 +0000
1178@@ -9,12 +9,34 @@
1179
1180 import glob
1181 import os
1182-import subprocess
1183 import shutil
1184 import sys
1185
1186 import ceph
1187-import utils
1188+from charmhelpers.core.hookenv import (
1189+ log,
1190+ ERROR,
1191+ config,
1192+ relation_ids,
1193+ related_units,
1194+ relation_get,
1195+ Hooks,
1196+ UnregisteredHookError
1197+)
1198+from charmhelpers.core.host import (
1199+ apt_install,
1200+ apt_update,
1201+ filter_installed_packages,
1202+ umount
1203+)
1204+from charmhelpers.fetch import add_source
1205+
1206+from utils import (
1207+ render_template,
1208+ get_host_ip,
1209+)
1210+
1211+hooks = Hooks()
1212
1213
1214 def install_upstart_scripts():
1215@@ -24,72 +46,72 @@
1216 shutil.copy(x, '/etc/init/')
1217
1218
1219+@hooks.hook('install')
1220 def install():
1221- utils.juju_log('INFO', 'Begin install hook.')
1222- utils.configure_source()
1223- utils.install('ceph', 'gdisk', 'ntp', 'btrfs-tools', 'xfsprogs')
1224+ log('Begin install hook.')
1225+ add_source(config('source'), config('key'))
1226+ apt_update(fatal=True)
1227+ apt_install(packages=ceph.PACKAGES, fatal=True)
1228 install_upstart_scripts()
1229- utils.juju_log('INFO', 'End install hook.')
1230+ log('End install hook.')
1231
1232
1233 def emit_cephconf():
1234 mon_hosts = get_mon_hosts()
1235- utils.juju_log('INFO', 'Monitor hosts are ' + repr(mon_hosts))
1236+ log('Monitor hosts are ' + repr(mon_hosts))
1237
1238 cephcontext = {
1239 'auth_supported': get_auth(),
1240 'mon_hosts': ' '.join(mon_hosts),
1241 'fsid': get_fsid(),
1242 'version': ceph.get_ceph_version()
1243- }
1244+ }
1245
1246 with open('/etc/ceph/ceph.conf', 'w') as cephconf:
1247- cephconf.write(utils.render_template('ceph.conf', cephcontext))
1248+ cephconf.write(render_template('ceph.conf', cephcontext))
1249
1250 JOURNAL_ZAPPED = '/var/lib/ceph/journal_zapped'
1251
1252
1253+@hooks.hook('config-changed')
1254 def config_changed():
1255- utils.juju_log('INFO', 'Begin config-changed hook.')
1256+ log('Begin config-changed hook.')
1257
1258 # Pre-flight checks
1259- if utils.config_get('osd-format') not in ceph.DISK_FORMATS:
1260- utils.juju_log('CRITICAL',
1261- 'Invalid OSD disk format configuration specified')
1262+ if config('osd-format') not in ceph.DISK_FORMATS:
1263+ log('Invalid OSD disk format configuration specified', level=ERROR)
1264 sys.exit(1)
1265
1266- e_mountpoint = utils.config_get('ephemeral-unmount')
1267- if (e_mountpoint and
1268- filesystem_mounted(e_mountpoint)):
1269- subprocess.call(['umount', e_mountpoint])
1270+ e_mountpoint = config('ephemeral-unmount')
1271+ if (e_mountpoint and ceph.filesystem_mounted(e_mountpoint)):
1272+ umount(e_mountpoint)
1273
1274- osd_journal = utils.config_get('osd-journal')
1275- if (osd_journal and
1276- not os.path.exists(JOURNAL_ZAPPED) and
1277- os.path.exists(osd_journal)):
1278+ osd_journal = config('osd-journal')
1279+ if (osd_journal and not os.path.exists(JOURNAL_ZAPPED)
1280+ and os.path.exists(osd_journal)):
1281 ceph.zap_disk(osd_journal)
1282 with open(JOURNAL_ZAPPED, 'w') as zapped:
1283 zapped.write('DONE')
1284
1285 if ceph.is_bootstrapped():
1286- utils.juju_log('INFO', 'ceph bootstrapped, rescanning disks')
1287+ log('ceph bootstrapped, rescanning disks')
1288 emit_cephconf()
1289- for dev in utils.config_get('osd-devices').split(' '):
1290- osdize(dev)
1291+ for dev in config('osd-devices').split(' '):
1292+ ceph.osdize(dev, config('osd-format'),
1293+ config('osd-journal'), config('osd-reformat'))
1294 ceph.rescan_osd_devices()
1295
1296- utils.juju_log('INFO', 'End config-changed hook.')
1297+ log('End config-changed hook.')
1298
1299
1300 def get_mon_hosts():
1301 hosts = []
1302- for relid in utils.relation_ids('mon'):
1303- for unit in utils.relation_list(relid):
1304+ for relid in relation_ids('mon'):
1305+ for unit in related_units(relid):
1306 hosts.append(
1307- '{}:6789'.format(utils.get_host_ip(
1308- utils.relation_get('private-address',
1309- unit, relid)))
1310- )
1311+ '{}:6789'.format(get_host_ip(relation_get('private-address',
1312+ unit, relid)))
1313+ )
1314
1315 hosts.sort()
1316 return hosts
1317@@ -104,103 +126,55 @@
1318
1319
1320 def get_conf(name):
1321- for relid in utils.relation_ids('mon'):
1322- for unit in utils.relation_list(relid):
1323- conf = utils.relation_get(name,
1324- unit, relid)
1325+ for relid in relation_ids('mon'):
1326+ for unit in related_units(relid):
1327+ conf = relation_get(name,
1328+ unit, relid)
1329 if conf:
1330 return conf
1331 return None
1332
1333
1334 def reformat_osd():
1335- if utils.config_get('osd-reformat'):
1336+ if config('osd-reformat'):
1337 return True
1338 else:
1339 return False
1340
1341
1342-def osdize(dev):
1343- if not os.path.exists(dev):
1344- utils.juju_log('INFO',
1345- 'Path {} does not exist - bailing'.format(dev))
1346- return
1347-
1348- if (ceph.is_osd_disk(dev) and not
1349- reformat_osd()):
1350- utils.juju_log('INFO',
1351- 'Looks like {} is already an OSD, skipping.'
1352- .format(dev))
1353- return
1354-
1355- if device_mounted(dev):
1356- utils.juju_log('INFO',
1357- 'Looks like {} is in use, skipping.'.format(dev))
1358- return
1359-
1360- cmd = ['ceph-disk-prepare']
1361- # Later versions of ceph support more options
1362- if ceph.get_ceph_version() >= "0.48.3":
1363- osd_format = utils.config_get('osd-format')
1364- if osd_format:
1365- cmd.append('--fs-type')
1366- cmd.append(osd_format)
1367- cmd.append(dev)
1368- osd_journal = utils.config_get('osd-journal')
1369- if (osd_journal and
1370- os.path.exists(osd_journal)):
1371- cmd.append(osd_journal)
1372- else:
1373- # Just provide the device - no other options
1374- # for older versions of ceph
1375- cmd.append(dev)
1376- subprocess.call(cmd)
1377-
1378-
1379-def device_mounted(dev):
1380- return subprocess.call(['grep', '-wqs', dev + '1', '/proc/mounts']) == 0
1381-
1382-
1383-def filesystem_mounted(fs):
1384- return subprocess.call(['grep', '-wqs', fs, '/proc/mounts']) == 0
1385-
1386-
1387+@hooks.hook('mon-relation-changed',
1388+ 'mon-relation-departed')
1389 def mon_relation():
1390- utils.juju_log('INFO', 'Begin mon-relation hook.')
1391+ log('Begin mon-relation hook.')
1392
1393- bootstrap_key = utils.relation_get('osd_bootstrap_key')
1394- if (get_fsid() and
1395- get_auth() and
1396- bootstrap_key):
1397- utils.juju_log('INFO', 'mon has provided conf- scanning disks')
1398+ bootstrap_key = relation_get('osd_bootstrap_key')
1399+ if get_fsid() and get_auth() and bootstrap_key:
1400+ log('mon has provided conf- scanning disks')
1401 emit_cephconf()
1402 ceph.import_osd_bootstrap_key(bootstrap_key)
1403- for dev in utils.config_get('osd-devices').split(' '):
1404- osdize(dev)
1405+ for dev in config('osd-devices').split(' '):
1406+ ceph.osdize(dev, config('osd-format'),
1407+ config('osd-journal'), config('osd-reformat'))
1408 ceph.rescan_osd_devices()
1409 else:
1410- utils.juju_log('INFO',
1411- 'mon cluster has not yet provided conf')
1412-
1413- utils.juju_log('INFO', 'End mon-relation hook.')
1414-
1415-
1416+ log('mon cluster has not yet provided conf')
1417+
1418+ log('End mon-relation hook.')
1419+
1420+
1421+@hooks.hook('upgrade-charm')
1422 def upgrade_charm():
1423- utils.juju_log('INFO', 'Begin upgrade-charm hook.')
1424- if (get_fsid() and
1425- get_auth()):
1426+ log('Begin upgrade-charm hook.')
1427+ if get_fsid() and get_auth():
1428 emit_cephconf()
1429 install_upstart_scripts()
1430- utils.install('xfsprogs')
1431- utils.juju_log('INFO', 'End upgrade-charm hook.')
1432-
1433-
1434-utils.do_hooks({
1435- 'config-changed': config_changed,
1436- 'install': install,
1437- 'mon-relation-departed': mon_relation,
1438- 'mon-relation-changed': mon_relation,
1439- 'upgrade-charm': upgrade_charm,
1440- })
1441-
1442-sys.exit(0)
1443+ apt_install(packages=filter_installed_packages(ceph.PACKAGES),
1444+ fatal=True)
1445+ log('End upgrade-charm hook.')
1446+
1447+
1448+if __name__ == '__main__':
1449+ try:
1450+ hooks.execute(sys.argv)
1451+ except UnregisteredHookError as e:
1452+ log('Unknown hook {} - skipping.'.format(e))
1453
1454=== modified file 'hooks/utils.py'
1455--- hooks/utils.py 2013-02-08 11:03:53 +0000
1456+++ hooks/utils.py 2013-07-08 08:34:32 +0000
1457@@ -7,97 +7,41 @@
1458 # Paul Collins <paul.collins@canonical.com>
1459 #
1460
1461-import os
1462-import subprocess
1463 import socket
1464-import sys
1465 import re
1466-
1467-
1468-def do_hooks(hooks):
1469- hook = os.path.basename(sys.argv[0])
1470-
1471- try:
1472- hook_func = hooks[hook]
1473- except KeyError:
1474- juju_log('INFO',
1475- "This charm doesn't know how to handle '{}'.".format(hook))
1476- else:
1477- hook_func()
1478-
1479-
1480-def install(*pkgs):
1481- cmd = [
1482- 'apt-get',
1483- '-y',
1484- 'install'
1485- ]
1486- for pkg in pkgs:
1487- cmd.append(pkg)
1488- subprocess.check_call(cmd)
1489+from charmhelpers.core.hookenv import (
1490+ unit_get,
1491+ cached
1492+)
1493+from charmhelpers.core.host import (
1494+ apt_install,
1495+ filter_installed_packages
1496+)
1497
1498 TEMPLATES_DIR = 'templates'
1499
1500 try:
1501 import jinja2
1502 except ImportError:
1503- install('python-jinja2')
1504+ apt_install(filter_installed_packages(['python-jinja2']),
1505+ fatal=True)
1506 import jinja2
1507
1508 try:
1509 import dns.resolver
1510 except ImportError:
1511- install('python-dnspython')
1512+ apt_install(filter_installed_packages(['python-dnspython']),
1513+ fatal=True)
1514 import dns.resolver
1515
1516
1517 def render_template(template_name, context, template_dir=TEMPLATES_DIR):
1518 templates = jinja2.Environment(
1519- loader=jinja2.FileSystemLoader(template_dir)
1520- )
1521+ loader=jinja2.FileSystemLoader(template_dir))
1522 template = templates.get_template(template_name)
1523 return template.render(context)
1524
1525
1526-CLOUD_ARCHIVE = \
1527-""" # Ubuntu Cloud Archive
1528-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1529-"""
1530-
1531-
1532-def configure_source():
1533- source = str(config_get('source'))
1534- if not source:
1535- return
1536- if source.startswith('ppa:'):
1537- cmd = [
1538- 'add-apt-repository',
1539- source
1540- ]
1541- subprocess.check_call(cmd)
1542- if source.startswith('cloud:'):
1543- install('ubuntu-cloud-keyring')
1544- pocket = source.split(':')[1]
1545- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
1546- apt.write(CLOUD_ARCHIVE.format(pocket))
1547- if source.startswith('http:'):
1548- with open('/etc/apt/sources.list.d/ceph.list', 'w') as apt:
1549- apt.write("deb " + source + "\n")
1550- key = config_get('key')
1551- if key:
1552- cmd = [
1553- 'apt-key',
1554- 'adv', '--keyserver keyserver.ubuntu.com',
1555- '--recv-keys', key
1556- ]
1557- subprocess.check_call(cmd)
1558- cmd = [
1559- 'apt-get',
1560- 'update'
1561- ]
1562- subprocess.check_call(cmd)
1563-
1564-
1565 def enable_pocket(pocket):
1566 apt_sources = "/etc/apt/sources.list"
1567 with open(apt_sources, "r") as sources:
1568@@ -109,105 +53,15 @@
1569 else:
1570 sources.write(line)
1571
1572-# Protocols
1573-TCP = 'TCP'
1574-UDP = 'UDP'
1575-
1576-
1577-def expose(port, protocol='TCP'):
1578- cmd = [
1579- 'open-port',
1580- '{}/{}'.format(port, protocol)
1581- ]
1582- subprocess.check_call(cmd)
1583-
1584-
1585-def juju_log(severity, message):
1586- cmd = [
1587- 'juju-log',
1588- '--log-level', severity,
1589- message
1590- ]
1591- subprocess.check_call(cmd)
1592-
1593-
1594-def relation_ids(relation):
1595- cmd = [
1596- 'relation-ids',
1597- relation
1598- ]
1599- return subprocess.check_output(cmd).split() # IGNORE:E1103
1600-
1601-
1602-def relation_list(rid):
1603- cmd = [
1604- 'relation-list',
1605- '-r', rid,
1606- ]
1607- return subprocess.check_output(cmd).split() # IGNORE:E1103
1608-
1609-
1610-def relation_get(attribute, unit=None, rid=None):
1611- cmd = [
1612- 'relation-get',
1613- ]
1614- if rid:
1615- cmd.append('-r')
1616- cmd.append(rid)
1617- cmd.append(attribute)
1618- if unit:
1619- cmd.append(unit)
1620- value = str(subprocess.check_output(cmd)).strip()
1621- if value == "":
1622- return None
1623- else:
1624- return value
1625-
1626-
1627-def relation_set(**kwargs):
1628- cmd = [
1629- 'relation-set'
1630- ]
1631- args = []
1632- for k, v in kwargs.items():
1633- if k == 'rid':
1634- cmd.append('-r')
1635- cmd.append(v)
1636- else:
1637- args.append('{}={}'.format(k, v))
1638- cmd += args
1639- subprocess.check_call(cmd)
1640-
1641-
1642-def unit_get(attribute):
1643- cmd = [
1644- 'unit-get',
1645- attribute
1646- ]
1647- value = str(subprocess.check_output(cmd)).strip()
1648- if value == "":
1649- return None
1650- else:
1651- return value
1652-
1653-
1654-def config_get(attribute):
1655- cmd = [
1656- 'config-get',
1657- attribute
1658- ]
1659- value = str(subprocess.check_output(cmd)).strip()
1660- if value == "":
1661- return None
1662- else:
1663- return value
1664-
1665-
1666+
1667+@cached
1668 def get_unit_hostname():
1669 return socket.gethostname()
1670
1671
1672-def get_host_ip(hostname=unit_get('private-address')):
1673+@cached
1674+def get_host_ip(hostname=None):
1675+ hostname = hostname or unit_get('private-address')
1676 try:
1677 # Test to see if already an IPv4 address
1678 socket.inet_aton(hostname)
1679
1680=== added file 'icon.svg'
1681--- icon.svg 1970-01-01 00:00:00 +0000
1682+++ icon.svg 2013-07-08 08:34:32 +0000
1683@@ -0,0 +1,414 @@
1684+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1685+<!-- Created with Inkscape (http://www.inkscape.org/) -->
1686+
1687+<svg
1688+ xmlns:dc="http://purl.org/dc/elements/1.1/"
1689+ xmlns:cc="http://creativecommons.org/ns#"
1690+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1691+ xmlns:svg="http://www.w3.org/2000/svg"
1692+ xmlns="http://www.w3.org/2000/svg"
1693+ xmlns:xlink="http://www.w3.org/1999/xlink"
1694+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1695+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1696+ width="96"
1697+ height="96"
1698+ id="svg6517"
1699+ version="1.1"
1700+ inkscape:version="0.48+devel r12304"
1701+ sodipodi:docname="ceph01.svg.2013_04_25_16_07_37.0.svg">
1702+ <defs
1703+ id="defs6519">
1704+ <linearGradient
1705+ id="Background">
1706+ <stop
1707+ id="stop4178"
1708+ offset="0"
1709+ style="stop-color:#22779e;stop-opacity:1" />
1710+ <stop
1711+ id="stop4180"
1712+ offset="1"
1713+ style="stop-color:#2991c0;stop-opacity:1" />
1714+ </linearGradient>
1715+ <filter
1716+ style="color-interpolation-filters:sRGB;"
1717+ inkscape:label="Inner Shadow"
1718+ id="filter1121">
1719+ <feFlood
1720+ flood-opacity="0.59999999999999998"
1721+ flood-color="rgb(0,0,0)"
1722+ result="flood"
1723+ id="feFlood1123" />
1724+ <feComposite
1725+ in="flood"
1726+ in2="SourceGraphic"
1727+ operator="out"
1728+ result="composite1"
1729+ id="feComposite1125" />
1730+ <feGaussianBlur
1731+ in="composite1"
1732+ stdDeviation="1"
1733+ result="blur"
1734+ id="feGaussianBlur1127" />
1735+ <feOffset
1736+ dx="0"
1737+ dy="2"
1738+ result="offset"
1739+ id="feOffset1129" />
1740+ <feComposite
1741+ in="offset"
1742+ in2="SourceGraphic"
1743+ operator="atop"
1744+ result="composite2"
1745+ id="feComposite1131" />
1746+ </filter>
1747+ <filter
1748+ style="color-interpolation-filters:sRGB;"
1749+ inkscape:label="Drop Shadow"
1750+ id="filter950">
1751+ <feFlood
1752+ flood-opacity="0.25"
1753+ flood-color="rgb(0,0,0)"
1754+ result="flood"
1755+ id="feFlood952" />
1756+ <feComposite
1757+ in="flood"
1758+ in2="SourceGraphic"
1759+ operator="in"
1760+ result="composite1"
1761+ id="feComposite954" />
1762+ <feGaussianBlur
1763+ in="composite1"
1764+ stdDeviation="1"
1765+ result="blur"
1766+ id="feGaussianBlur956" />
1767+ <feOffset
1768+ dx="0"
1769+ dy="1"
1770+ result="offset"
1771+ id="feOffset958" />
1772+ <feComposite
1773+ in="SourceGraphic"
1774+ in2="offset"
1775+ operator="over"
1776+ result="composite2"
1777+ id="feComposite960" />
1778+ </filter>
1779+ <clipPath
1780+ clipPathUnits="userSpaceOnUse"
1781+ id="clipPath873">
1782+ <g
1783+ transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
1784+ id="g875"
1785+ inkscape:label="Layer 1"
1786+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
1787+ <path
1788+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
1789+ 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"
1790+ id="path877"
1791+ inkscape:connector-curvature="0"
1792+ sodipodi:nodetypes="sssssssss" />
1793+ </g>
1794+ </clipPath>
1795+ <filter
1796+ inkscape:collect="always"
1797+ id="filter891"
1798+ inkscape:label="Badge Shadow">
1799+ <feGaussianBlur
1800+ inkscape:collect="always"
1801+ stdDeviation="0.71999962"
1802+ id="feGaussianBlur893" />
1803+ </filter>
1804+ <style
1805+ id="style867"
1806+ type="text/css"><![CDATA[
1807+ .fil0 {fill:#1F1A17}
1808+ ]]></style>
1809+ <linearGradient
1810+ inkscape:collect="always"
1811+ xlink:href="#linearGradient4439"
1812+ id="linearGradient908"
1813+ x1="-220"
1814+ y1="731.29077"
1815+ x2="-220"
1816+ y2="635.29077"
1817+ gradientUnits="userSpaceOnUse" />
1818+ <clipPath
1819+ id="clipPath16">
1820+ <path
1821+ id="path18"
1822+ d="m -9,-9 614,0 0,231 -614,0 0,-231 z" />
1823+ </clipPath>
1824+ <clipPath
1825+ id="clipPath116">
1826+ <path
1827+ id="path118"
1828+ 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" />
1829+ </clipPath>
1830+ <clipPath
1831+ id="clipPath128">
1832+ <path
1833+ id="path130"
1834+ 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" />
1835+ </clipPath>
1836+ <linearGradient
1837+ id="linearGradient3850"
1838+ inkscape:collect="always">
1839+ <stop
1840+ id="stop3852"
1841+ offset="0"
1842+ style="stop-color:#000000;stop-opacity:1;" />
1843+ <stop
1844+ id="stop3854"
1845+ offset="1"
1846+ style="stop-color:#000000;stop-opacity:0;" />
1847+ </linearGradient>
1848+ <clipPath
1849+ clipPathUnits="userSpaceOnUse"
1850+ id="clipPath3095">
1851+ <path
1852+ d="m 976.648,389.551 -842.402,0 0,839.999 842.402,0 0,-839.999"
1853+ id="path3097"
1854+ inkscape:connector-curvature="0" />
1855+ </clipPath>
1856+ <linearGradient
1857+ id="linearGradient4439"
1858+ inkscape:collect="always">
1859+ <stop
1860+ id="stop4441"
1861+ offset="0"
1862+ style="stop-color:#e3e3e3;stop-opacity:1" />
1863+ <stop
1864+ id="stop4443"
1865+ offset="1"
1866+ style="stop-color:#efefef;stop-opacity:1" />
1867+ </linearGradient>
1868+ <clipPath
1869+ clipPathUnits="userSpaceOnUse"
1870+ id="clipPath3195">
1871+ <path
1872+ 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"
1873+ id="path3197"
1874+ inkscape:connector-curvature="0" />
1875+ </clipPath>
1876+ <clipPath
1877+ clipPathUnits="userSpaceOnUse"
1878+ id="clipPath3235">
1879+ <path
1880+ 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"
1881+ id="path3237"
1882+ inkscape:connector-curvature="0" />
1883+ </clipPath>
1884+ <clipPath
1885+ id="clipPath4591"
1886+ clipPathUnits="userSpaceOnUse">
1887+ <path
1888+ inkscape:connector-curvature="0"
1889+ 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"
1890+ style="fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
1891+ id="path4593" />
1892+ </clipPath>
1893+ <radialGradient
1894+ gradientUnits="userSpaceOnUse"
1895+ gradientTransform="matrix(-1.4333926,-2.2742838,1.1731823,-0.73941125,-174.08025,98.374394)"
1896+ r="20.40658"
1897+ fy="93.399292"
1898+ fx="-26.508606"
1899+ cy="93.399292"
1900+ cx="-26.508606"
1901+ id="radialGradient3856"
1902+ xlink:href="#linearGradient3850"
1903+ inkscape:collect="always" />
1904+ <linearGradient
1905+ gradientTransform="translate(-318.48033,212.32022)"
1906+ gradientUnits="userSpaceOnUse"
1907+ y2="993.19702"
1908+ x2="-51.879555"
1909+ y1="593.11615"
1910+ x1="348.20132"
1911+ id="linearGradient3895"
1912+ xlink:href="#linearGradient3850"
1913+ inkscape:collect="always" />
1914+ <clipPath
1915+ id="clipPath3906"
1916+ clipPathUnits="userSpaceOnUse">
1917+ <rect
1918+ transform="scale(1,-1)"
1919+ style="opacity:0.8;color:#000000;fill:#ff00ff;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
1920+ id="rect3908"
1921+ width="1019.1371"
1922+ height="1019.1371"
1923+ x="357.9816"
1924+ y="-1725.8152" />
1925+ </clipPath>
1926+ </defs>
1927+ <sodipodi:namedview
1928+ id="base"
1929+ pagecolor="#ffffff"
1930+ bordercolor="#666666"
1931+ borderopacity="1.0"
1932+ inkscape:pageopacity="0.0"
1933+ inkscape:pageshadow="2"
1934+ inkscape:zoom="3.2596289"
1935+ inkscape:cx="55.171524"
1936+ inkscape:cy="27.879339"
1937+ inkscape:document-units="px"
1938+ inkscape:current-layer="layer1"
1939+ showgrid="false"
1940+ fit-margin-top="0"
1941+ fit-margin-left="0"
1942+ fit-margin-right="0"
1943+ fit-margin-bottom="0"
1944+ inkscape:window-width="1920"
1945+ inkscape:window-height="1029"
1946+ inkscape:window-x="0"
1947+ inkscape:window-y="24"
1948+ inkscape:window-maximized="1"
1949+ showborder="true"
1950+ showguides="false"
1951+ inkscape:guide-bbox="true"
1952+ inkscape:showpageshadow="false"
1953+ inkscape:snap-global="true"
1954+ inkscape:snap-bbox="true"
1955+ inkscape:bbox-paths="true"
1956+ inkscape:bbox-nodes="true"
1957+ inkscape:snap-bbox-edge-midpoints="true"
1958+ inkscape:snap-bbox-midpoints="true"
1959+ inkscape:object-paths="true"
1960+ inkscape:snap-intersection-paths="true"
1961+ inkscape:object-nodes="true"
1962+ inkscape:snap-smooth-nodes="true"
1963+ inkscape:snap-midpoints="true"
1964+ inkscape:snap-object-midpoints="true"
1965+ inkscape:snap-center="true">
1966+ <inkscape:grid
1967+ type="xygrid"
1968+ id="grid821" />
1969+ <sodipodi:guide
1970+ orientation="1,0"
1971+ position="16,48"
1972+ id="guide823" />
1973+ <sodipodi:guide
1974+ orientation="0,1"
1975+ position="64,80"
1976+ id="guide825" />
1977+ <sodipodi:guide
1978+ orientation="1,0"
1979+ position="80,40"
1980+ id="guide827" />
1981+ <sodipodi:guide
1982+ orientation="0,1"
1983+ position="64,16"
1984+ id="guide829" />
1985+ </sodipodi:namedview>
1986+ <metadata
1987+ id="metadata6522">
1988+ <rdf:RDF>
1989+ <cc:Work
1990+ rdf:about="">
1991+ <dc:format>image/svg+xml</dc:format>
1992+ <dc:type
1993+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
1994+ <dc:title></dc:title>
1995+ </cc:Work>
1996+ </rdf:RDF>
1997+ </metadata>
1998+ <g
1999+ inkscape:label="BACKGROUND"
2000+ inkscape:groupmode="layer"
2001+ id="layer1"
2002+ transform="translate(268,-635.29076)"
2003+ style="display:inline">
2004+ <path
2005+ style="fill:url(#linearGradient908);fill-opacity:1.0;stroke:none;display:inline;filter:url(#filter1121)"
2006+ 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"
2007+ id="path6455"
2008+ inkscape:connector-curvature="0"
2009+ sodipodi:nodetypes="sssssssss" />
2010+ <path
2011+ style="color:#000000;fill:#f05c56;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2012+ d="M 48 17 C 30.879173 17 17 30.879173 17 48 C 17 58.205647 21.921021 67.257453 29.53125 72.90625 C 31.106248 70.853104 31.792246 69.428164 32 68.65625 C 32.24185 67.75765 32.1873 67.60443 31.96875 66.875 C 31.775272 66.229265 30.9225 65.268196 29.5625 63.40625 C 29.543376 63.380067 29.519319 63.339038 29.5 63.3125 C 29.490587 63.301137 29.478142 63.29263 29.46875 63.28125 C 28.411032 61.999592 27.526437 60.578409 26.75 59.09375 C 25.013179 55.772698 24 52.007471 24 48 C 24 40.95011 27.065992 34.640853 31.90625 30.25 C 36.166391 26.385401 41.795056 24 48 24 C 54.213203 24 59.862908 26.376233 64.125 30.25 C 64.693279 30.766502 65.233498 31.306721 65.75 31.875 C 65.76468 31.891254 65.766619 31.92121 65.78125 31.9375 C 69.628213 36.193847 72 41.810964 72 48 C 72 53.79899 69.954991 59.132635 66.53125 63.28125 C 66.523757 63.291557 66.507464 63.302247 66.5 63.3125 C 66.476288 63.341126 66.461343 63.377737 66.4375 63.40625 C 65.0775 65.268196 64.224728 66.229265 64.03125 66.875 C 63.8127 67.60443 63.75815 67.75765 64 68.65625 C 64.207754 69.428164 64.893752 70.853104 66.46875 72.90625 C 74.078979 67.257453 79 58.205647 79 48 C 79 30.879173 65.120827 17 48 17 z M 48 30 C 44.47123 30.006 41.00461 31.05306 38.0625 33 C 29.81314 38.45904 27.5261 49.65783 32.96875 57.90625 C 32.97675 57.91845 32.992 57.9254 33 57.9375 C 34.69385 60.49644 36.81536 62.34977 37.65625 65.15625 C 38.417511 67.69696 38.464725 70.888363 34.6875 76 C 36.843125 77.026739 39.136156 77.803912 41.53125 78.3125 C 45.121429 72.505399 45.6 67.244681 44.375 63.15625 C 42.8785 58.16169 39.28615 54.78175 38.8125 54.0625 L 38.84375 54.0625 C 35.46092 48.96255 36.83156 42.19137 41.9375 38.8125 C 43.73298 37.62434 45.84671 37.00604 48 37 C 48.0104 37 48.02095 36.99997 48.03125 37 C 50.18163 37.006 52.26946 37.62456 54.0625 38.8125 C 59.16152 42.19075 60.56575 48.96347 57.1875 54.0625 C 56.71385 54.78175 53.1215 58.16169 51.625 63.15625 C 50.4 67.244681 50.878571 72.505399 54.46875 78.3125 C 56.863844 77.803912 59.156875 77.026739 61.3125 76 C 57.535275 70.888363 57.582489 67.69696 58.34375 65.15625 C 59.18464 62.34977 61.30615 60.49644 63 57.9375 C 68.46768 49.68475 66.19025 38.46769 57.9375 33 C 54.99872 31.05298 51.55602 30.00613 48.03125 30 C 48.0209 30.00003 48.0105 30 48 30 z M 48 43 C 45.238576 43 43 45.238576 43 48 C 43 50.761424 45.238576 53 48 53 C 50.761424 53 53 50.761424 53 48 C 53 45.238576 50.761424 43 48 43 z M 48 78.6875 C 47.94945 78.791177 47.895343 78.896359 47.84375 79 C 47.896122 79.00026 47.947567 79 48 79 C 48.052433 79 48.103878 79.00026 48.15625 79 C 48.104657 78.896359 48.05055 78.791177 48 78.6875 z "
2013+ transform="translate(-268,635.29076)"
2014+ id="path939" />
2015+ <path
2016+ style="color:#000000;fill:#dd322b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2017+ d="M 48 17 C 30.879173 17 17 30.879173 17 48 C 17 48.125345 16.99852 48.250008 17 48.375 C 17.334303 31.543929 31.088903 18 48 18 C 64.911097 18 78.665697 31.543929 79 48.375 C 79.00148 48.250008 79 48.125345 79 48 C 79 30.879173 65.120827 17 48 17 z M 48 30 C 44.47123 30.006 41.00461 31.05306 38.0625 33 C 32.70971 36.542226 29.880485 42.49862 30.03125 48.5 C 30.204405 42.846736 33.009903 37.343572 38.0625 34 C 41.00461 32.05306 44.47123 31.006 48 31 C 48.0105 31 48.02105 31.00008 48.03125 31 C 51.55602 31.00613 54.99872 32.05298 57.9375 34 C 62.975581 37.337877 65.755795 42.827998 65.9375 48.46875 C 66.07621 42.48142 63.273758 36.535428 57.9375 33 C 54.99872 31.05298 51.55602 30.00613 48.03125 30 C 48.0209 30.00003 48.0105 30 48 30 z M 48 43 C 45.238576 43 43 45.238576 43 48 C 43 48.172589 43.01418 48.331915 43.03125 48.5 C 43.2873 45.978722 45.411169 44 48 44 C 50.588831 44 52.7127 45.978722 52.96875 48.5 C 52.98582 48.331915 53 48.172589 53 48 C 53 45.238576 50.761424 43 48 43 z M 24 48.375 C 23.994609 48.583358 24 48.790301 24 49 C 24 53.00747 25.01318 56.7727 26.75 60.09375 C 27.52644 61.57841 28.41103 62.99959 29.46875 64.28125 C 29.47805 64.29255 29.491 64.3011 29.5 64.3125 C 29.5193 64.339 29.5434 64.38005 29.5625 64.40625 C 30.9225 66.2682 31.77527 67.22927 31.96875 67.875 C 32.016575 68.034657 32.063023 68.162945 32.09375 68.28125 C 32.215432 67.710404 32.150661 67.482143 31.96875 66.875 C 31.775272 66.229265 30.9225 65.268196 29.5625 63.40625 C 29.543376 63.380067 29.519319 63.339038 29.5 63.3125 C 29.490587 63.301137 29.478142 63.29263 29.46875 63.28125 C 28.411032 61.999592 27.526437 60.578409 26.75 59.09375 C 25.067361 55.876301 24.060969 52.242622 24 48.375 z M 71.96875 48.375 C 71.881931 54.027547 69.880433 59.222978 66.53125 63.28125 C 66.523757 63.291557 66.507464 63.302247 66.5 63.3125 C 66.476288 63.341126 66.461343 63.377737 66.4375 63.40625 C 65.0775 65.268196 64.224728 66.229265 64.03125 66.875 C 63.849339 67.482143 63.784568 67.710404 63.90625 68.28125 C 63.936983 68.162945 63.983414 68.034657 64.03125 67.875 C 64.22473 67.22927 65.0775 66.2682 66.4375 64.40625 C 66.4613 64.37775 66.4763 64.3411 66.5 64.3125 C 66.5074 64.3023 66.52425 64.29155 66.53125 64.28125 C 69.95499 60.13264 72 54.79899 72 49 C 72 48.790781 71.974136 48.582881 71.96875 48.375 z M 37.03125 48.5625 C 36.950597 50.780966 37.510968 53.053199 38.84375 55.0625 L 38.8125 55.0625 C 39.28615 55.78175 42.8785 59.16169 44.375 64.15625 C 44.74962 65.406541 44.953039 66.767092 44.96875 68.21875 C 45.062756 66.388889 44.834375 64.689412 44.375 63.15625 C 42.8785 58.16169 39.28615 54.78175 38.8125 54.0625 L 38.84375 54.0625 C 37.712059 52.356364 37.137075 50.453131 37.03125 48.5625 z M 59 48.5625 C 58.899597 50.453201 58.317659 52.356672 57.1875 54.0625 C 56.71385 54.78175 53.1215 58.16169 51.625 63.15625 C 51.165625 64.689412 50.937244 66.388889 51.03125 68.21875 C 51.046961 66.767092 51.25038 65.406541 51.625 64.15625 C 53.1215 59.16169 56.71385 55.78175 57.1875 55.0625 C 58.518478 53.053561 59.087212 50.781084 59 48.5625 z M 38.03125 68.5 C 37.866539 70.476556 36.990243 72.883747 34.6875 76 C 34.858955 76.081665 35.045574 76.140252 35.21875 76.21875 C 37.469311 72.948813 38.106912 70.501999 38.03125 68.5 z M 57.96875 68.5 C 57.893088 70.501999 58.530689 72.948813 60.78125 76.21875 C 60.954426 76.140252 61.141045 76.081665 61.3125 76 C 59.009757 72.883747 58.133461 70.476556 57.96875 68.5 z M 48 78.6875 C 47.94945 78.791177 47.895343 78.896359 47.84375 79 C 47.896122 79.00026 47.947567 79 48 79 C 48.052433 79 48.103878 79.00026 48.15625 79 C 48.104657 78.896359 48.05055 78.791177 48 78.6875 z "
2018+ transform="translate(-268,635.29076)"
2019+ id="path899" />
2020+ </g>
2021+ <g
2022+ inkscape:groupmode="layer"
2023+ id="layer3"
2024+ inkscape:label="PLACE YOUR PICTOGRAM HERE"
2025+ style="display:inline" />
2026+ <g
2027+ inkscape:groupmode="layer"
2028+ id="layer2"
2029+ inkscape:label="BADGE"
2030+ style="display:none"
2031+ sodipodi:insensitive="true">
2032+ <g
2033+ style="display:inline"
2034+ transform="translate(-340.00001,-581)"
2035+ id="g4394"
2036+ clip-path="none">
2037+ <g
2038+ id="g855">
2039+ <g
2040+ inkscape:groupmode="maskhelper"
2041+ id="g870"
2042+ clip-path="url(#clipPath873)"
2043+ style="opacity:0.6;filter:url(#filter891)">
2044+ <path
2045+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
2046+ 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 C 258.62742,540.36218 264,545.73477 264,552.36218 Z"
2047+ sodipodi:ry="12"
2048+ sodipodi:rx="12"
2049+ sodipodi:cy="552.36218"
2050+ sodipodi:cx="252"
2051+ id="path844"
2052+ 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"
2053+ sodipodi:type="arc" />
2054+ </g>
2055+ <g
2056+ id="g862">
2057+ <path
2058+ sodipodi:type="arc"
2059+ 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"
2060+ id="path4398"
2061+ sodipodi:cx="252"
2062+ sodipodi:cy="552.36218"
2063+ sodipodi:rx="12"
2064+ sodipodi:ry="12"
2065+ 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 C 258.62742,540.36218 264,545.73477 264,552.36218 Z"
2066+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
2067+ <path
2068+ transform="matrix(1.25,0,0,1.25,33,-100.45273)"
2069+ 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 C 258.62742,540.36218 264,545.73477 264,552.36218 Z"
2070+ sodipodi:ry="12"
2071+ sodipodi:rx="12"
2072+ sodipodi:cy="552.36218"
2073+ sodipodi:cx="252"
2074+ id="path4400"
2075+ 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"
2076+ sodipodi:type="arc" />
2077+ <path
2078+ sodipodi:type="star"
2079+ 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"
2080+ id="path4459"
2081+ sodipodi:sides="5"
2082+ sodipodi:cx="666.19574"
2083+ sodipodi:cy="589.50385"
2084+ sodipodi:r1="7.2431178"
2085+ sodipodi:r2="4.3458705"
2086+ sodipodi:arg1="1.0471976"
2087+ sodipodi:arg2="1.6755161"
2088+ inkscape:flatsided="false"
2089+ inkscape:rounded="0.1"
2090+ inkscape:randomized="0"
2091+ 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 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"
2092+ transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
2093+ </g>
2094+ </g>
2095+ </g>
2096+ </g>
2097+</svg>

Subscribers

People subscribed via source and target branches