Merge lp:~stub/charms/precise/postgresql-psql/trunk into lp:charms/postgresql-psql

Proposed by Stuart Bishop
Status: Merged
Approved by: Stuart Bishop
Approved revision: 20
Merged at revision: 16
Proposed branch: lp:~stub/charms/precise/postgresql-psql/trunk
Merge into: lp:charms/postgresql-psql
Diff against target: 1261 lines (+994/-171)
5 files modified
charm-helpers.yaml (+4/-0)
hooks/charmhelpers/core/hookenv.py (+339/-0)
hooks/charmhelpers/core/host.py (+272/-0)
hooks/hooks.py (+36/-94)
icon.svg (+343/-77)
To merge this branch: bzr merge lp:~stub/charms/precise/postgresql-psql/trunk
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Review via email: mp+174773@code.launchpad.net

Commit message

Wait until the new 'allowed-units' property tells us permissions have been granted before creating database access scripts.

Description of the change

The first change on this branch is to make use of the 'allowed-units' setting on the relation, allowing us to confirm that https://code.launchpad.net/~stub/charms/precise/postgresql/bug-1187508-allowed-hosts/+merge/174771 works as intended. This branch should not land until that MP has landed on the PostgreSQL charm.

The bulk of the changes is tearing out our helpers and using charm-helpers instead. The included version of charm-helpers is current trunk.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Lines 755-760 are the new feature.

20. By Stuart Bishop

New icon using template

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

lgtm

review: Approve
Revision history for this message
Stuart Bishop (stub) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'charm-helpers.yaml'
2--- charm-helpers.yaml 1970-01-01 00:00:00 +0000
3+++ charm-helpers.yaml 2013-07-16 12:39:27 +0000
4@@ -0,0 +1,4 @@
5+destination: hooks/charmhelpers
6+branch: lp:charm-helpers
7+include:
8+ - core
9
10=== added directory 'hooks/charmhelpers'
11=== added file 'hooks/charmhelpers/__init__.py'
12=== added directory 'hooks/charmhelpers/core'
13=== added file 'hooks/charmhelpers/core/__init__.py'
14=== added file 'hooks/charmhelpers/core/hookenv.py'
15--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
16+++ hooks/charmhelpers/core/hookenv.py 2013-07-16 12:39:27 +0000
17@@ -0,0 +1,339 @@
18+"Interactions with the Juju environment"
19+# Copyright 2013 Canonical Ltd.
20+#
21+# Authors:
22+# Charm Helpers Developers <juju@lists.ubuntu.com>
23+
24+import os
25+import json
26+import yaml
27+import subprocess
28+import UserDict
29+
30+CRITICAL = "CRITICAL"
31+ERROR = "ERROR"
32+WARNING = "WARNING"
33+INFO = "INFO"
34+DEBUG = "DEBUG"
35+MARKER = object()
36+
37+cache = {}
38+
39+
40+def cached(func):
41+ ''' Cache return values for multiple executions of func + args
42+
43+ For example:
44+
45+ @cached
46+ def unit_get(attribute):
47+ pass
48+
49+ unit_get('test')
50+
51+ will cache the result of unit_get + 'test' for future calls.
52+ '''
53+ def wrapper(*args, **kwargs):
54+ global cache
55+ key = str((func, args, kwargs))
56+ try:
57+ return cache[key]
58+ except KeyError:
59+ res = func(*args, **kwargs)
60+ cache[key] = res
61+ return res
62+ return wrapper
63+
64+
65+def flush(key):
66+ ''' Flushes any entries from function cache where the
67+ key is found in the function+args '''
68+ flush_list = []
69+ for item in cache:
70+ if key in item:
71+ flush_list.append(item)
72+ for item in flush_list:
73+ del cache[item]
74+
75+
76+def log(message, level=None):
77+ "Write a message to the juju log"
78+ command = ['juju-log']
79+ if level:
80+ command += ['-l', level]
81+ command += [message]
82+ subprocess.call(command)
83+
84+
85+class Serializable(UserDict.IterableUserDict):
86+ "Wrapper, an object that can be serialized to yaml or json"
87+
88+ def __init__(self, obj):
89+ # wrap the object
90+ UserDict.IterableUserDict.__init__(self)
91+ self.data = obj
92+
93+ def __getattr__(self, attr):
94+ # See if this object has attribute.
95+ if attr in ("json", "yaml", "data"):
96+ return self.__dict__[attr]
97+ # Check for attribute in wrapped object.
98+ got = getattr(self.data, attr, MARKER)
99+ if got is not MARKER:
100+ return got
101+ # Proxy to the wrapped object via dict interface.
102+ try:
103+ return self.data[attr]
104+ except KeyError:
105+ raise AttributeError(attr)
106+
107+ def __getstate__(self):
108+ # Pickle as a standard dictionary.
109+ return self.data
110+
111+ def __setstate__(self, state):
112+ # Unpickle into our wrapper.
113+ self.data = state
114+
115+ def json(self):
116+ "Serialize the object to json"
117+ return json.dumps(self.data)
118+
119+ def yaml(self):
120+ "Serialize the object to yaml"
121+ return yaml.dump(self.data)
122+
123+
124+def execution_environment():
125+ """A convenient bundling of the current execution context"""
126+ context = {}
127+ context['conf'] = config()
128+ if relation_id():
129+ context['reltype'] = relation_type()
130+ context['relid'] = relation_id()
131+ context['rel'] = relation_get()
132+ context['unit'] = local_unit()
133+ context['rels'] = relations()
134+ context['env'] = os.environ
135+ return context
136+
137+
138+def in_relation_hook():
139+ "Determine whether we're running in a relation hook"
140+ return 'JUJU_RELATION' in os.environ
141+
142+
143+def relation_type():
144+ "The scope for the current relation hook"
145+ return os.environ.get('JUJU_RELATION', None)
146+
147+
148+def relation_id():
149+ "The relation ID for the current relation hook"
150+ return os.environ.get('JUJU_RELATION_ID', None)
151+
152+
153+def local_unit():
154+ "Local unit ID"
155+ return os.environ['JUJU_UNIT_NAME']
156+
157+
158+def remote_unit():
159+ "The remote unit for the current relation hook"
160+ return os.environ['JUJU_REMOTE_UNIT']
161+
162+
163+def service_name():
164+ "The name service group this unit belongs to"
165+ return local_unit().split('/')[0]
166+
167+
168+@cached
169+def config(scope=None):
170+ "Juju charm configuration"
171+ config_cmd_line = ['config-get']
172+ if scope is not None:
173+ config_cmd_line.append(scope)
174+ config_cmd_line.append('--format=json')
175+ try:
176+ return json.loads(subprocess.check_output(config_cmd_line))
177+ except ValueError:
178+ return None
179+
180+
181+@cached
182+def relation_get(attribute=None, unit=None, rid=None):
183+ _args = ['relation-get', '--format=json']
184+ if rid:
185+ _args.append('-r')
186+ _args.append(rid)
187+ _args.append(attribute or '-')
188+ if unit:
189+ _args.append(unit)
190+ try:
191+ return json.loads(subprocess.check_output(_args))
192+ except ValueError:
193+ return None
194+
195+
196+def relation_set(relation_id=None, relation_settings={}, **kwargs):
197+ relation_cmd_line = ['relation-set']
198+ if relation_id is not None:
199+ relation_cmd_line.extend(('-r', relation_id))
200+ for k, v in (relation_settings.items() + kwargs.items()):
201+ if v is None:
202+ relation_cmd_line.append('{}='.format(k))
203+ else:
204+ relation_cmd_line.append('{}={}'.format(k, v))
205+ subprocess.check_call(relation_cmd_line)
206+ # Flush cache of any relation-gets for local unit
207+ flush(local_unit())
208+
209+
210+@cached
211+def relation_ids(reltype=None):
212+ "A list of relation_ids"
213+ reltype = reltype or relation_type()
214+ relid_cmd_line = ['relation-ids', '--format=json']
215+ if reltype is not None:
216+ relid_cmd_line.append(reltype)
217+ return json.loads(subprocess.check_output(relid_cmd_line)) or []
218+ return []
219+
220+
221+@cached
222+def related_units(relid=None):
223+ "A list of related units"
224+ relid = relid or relation_id()
225+ units_cmd_line = ['relation-list', '--format=json']
226+ if relid is not None:
227+ units_cmd_line.extend(('-r', relid))
228+ return json.loads(subprocess.check_output(units_cmd_line)) or []
229+
230+
231+@cached
232+def relation_for_unit(unit=None, rid=None):
233+ "Get the json represenation of a unit's relation"
234+ unit = unit or remote_unit()
235+ relation = relation_get(unit=unit, rid=rid)
236+ for key in relation:
237+ if key.endswith('-list'):
238+ relation[key] = relation[key].split()
239+ relation['__unit__'] = unit
240+ return relation
241+
242+
243+@cached
244+def relations_for_id(relid=None):
245+ "Get relations of a specific relation ID"
246+ relation_data = []
247+ relid = relid or relation_ids()
248+ for unit in related_units(relid):
249+ unit_data = relation_for_unit(unit, relid)
250+ unit_data['__relid__'] = relid
251+ relation_data.append(unit_data)
252+ return relation_data
253+
254+
255+@cached
256+def relations_of_type(reltype=None):
257+ "Get relations of a specific type"
258+ relation_data = []
259+ reltype = reltype or relation_type()
260+ for relid in relation_ids(reltype):
261+ for relation in relations_for_id(relid):
262+ relation['__relid__'] = relid
263+ relation_data.append(relation)
264+ return relation_data
265+
266+
267+@cached
268+def relation_types():
269+ "Get a list of relation types supported by this charm"
270+ charmdir = os.environ.get('CHARM_DIR', '')
271+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
272+ md = yaml.safe_load(mdf)
273+ rel_types = []
274+ for key in ('provides', 'requires', 'peers'):
275+ section = md.get(key)
276+ if section:
277+ rel_types.extend(section.keys())
278+ mdf.close()
279+ return rel_types
280+
281+
282+@cached
283+def relations():
284+ rels = {}
285+ for reltype in relation_types():
286+ relids = {}
287+ for relid in relation_ids(reltype):
288+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
289+ for unit in related_units(relid):
290+ reldata = relation_get(unit=unit, rid=relid)
291+ units[unit] = reldata
292+ relids[relid] = units
293+ rels[reltype] = relids
294+ return rels
295+
296+
297+def open_port(port, protocol="TCP"):
298+ "Open a service network port"
299+ _args = ['open-port']
300+ _args.append('{}/{}'.format(port, protocol))
301+ subprocess.check_call(_args)
302+
303+
304+def close_port(port, protocol="TCP"):
305+ "Close a service network port"
306+ _args = ['close-port']
307+ _args.append('{}/{}'.format(port, protocol))
308+ subprocess.check_call(_args)
309+
310+
311+@cached
312+def unit_get(attribute):
313+ _args = ['unit-get', '--format=json', attribute]
314+ try:
315+ return json.loads(subprocess.check_output(_args))
316+ except ValueError:
317+ return None
318+
319+
320+def unit_private_ip():
321+ return unit_get('private-address')
322+
323+
324+class UnregisteredHookError(Exception):
325+ pass
326+
327+
328+class Hooks(object):
329+ def __init__(self):
330+ super(Hooks, self).__init__()
331+ self._hooks = {}
332+
333+ def register(self, name, function):
334+ self._hooks[name] = function
335+
336+ def execute(self, args):
337+ hook_name = os.path.basename(args[0])
338+ if hook_name in self._hooks:
339+ self._hooks[hook_name]()
340+ else:
341+ raise UnregisteredHookError(hook_name)
342+
343+ def hook(self, *hook_names):
344+ def wrapper(decorated):
345+ for hook_name in hook_names:
346+ self.register(hook_name, decorated)
347+ else:
348+ self.register(decorated.__name__, decorated)
349+ if '_' in decorated.__name__:
350+ self.register(
351+ decorated.__name__.replace('_', '-'), decorated)
352+ return decorated
353+ return wrapper
354+
355+def charm_dir():
356+ return os.environ.get('CHARM_DIR')
357
358=== added file 'hooks/charmhelpers/core/host.py'
359--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
360+++ hooks/charmhelpers/core/host.py 2013-07-16 12:39:27 +0000
361@@ -0,0 +1,272 @@
362+"""Tools for working with the host system"""
363+# Copyright 2012 Canonical Ltd.
364+#
365+# Authors:
366+# Nick Moffitt <nick.moffitt@canonical.com>
367+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
368+
369+import apt_pkg
370+import os
371+import pwd
372+import grp
373+import subprocess
374+import hashlib
375+
376+from collections import OrderedDict
377+
378+from hookenv import log, execution_environment
379+
380+
381+def service_start(service_name):
382+ service('start', service_name)
383+
384+
385+def service_stop(service_name):
386+ service('stop', service_name)
387+
388+
389+def service_restart(service_name):
390+ service('restart', service_name)
391+
392+
393+def service_reload(service_name, restart_on_failure=False):
394+ if not service('reload', service_name) and restart_on_failure:
395+ service('restart', service_name)
396+
397+
398+def service(action, service_name):
399+ cmd = ['service', service_name, action]
400+ return subprocess.call(cmd) == 0
401+
402+
403+def service_running(service):
404+ try:
405+ output = subprocess.check_output(['service', service, 'status'])
406+ except subprocess.CalledProcessError:
407+ return False
408+ else:
409+ if ("start/running" in output or "is running" in output):
410+ return True
411+ else:
412+ return False
413+
414+
415+def adduser(username, password=None, shell='/bin/bash', system_user=False):
416+ """Add a user"""
417+ try:
418+ user_info = pwd.getpwnam(username)
419+ log('user {0} already exists!'.format(username))
420+ except KeyError:
421+ log('creating user {0}'.format(username))
422+ cmd = ['useradd']
423+ if system_user or password is None:
424+ cmd.append('--system')
425+ else:
426+ cmd.extend([
427+ '--create-home',
428+ '--shell', shell,
429+ '--password', password,
430+ ])
431+ cmd.append(username)
432+ subprocess.check_call(cmd)
433+ user_info = pwd.getpwnam(username)
434+ return user_info
435+
436+
437+def add_user_to_group(username, group):
438+ """Add a user to a group"""
439+ cmd = [
440+ 'gpasswd', '-a',
441+ username,
442+ group
443+ ]
444+ log("Adding user {} to group {}".format(username, group))
445+ subprocess.check_call(cmd)
446+
447+
448+def rsync(from_path, to_path, flags='-r', options=None):
449+ """Replicate the contents of a path"""
450+ context = execution_environment()
451+ options = options or ['--delete', '--executability']
452+ cmd = ['/usr/bin/rsync', flags]
453+ cmd.extend(options)
454+ cmd.append(from_path.format(**context))
455+ cmd.append(to_path.format(**context))
456+ log(" ".join(cmd))
457+ return subprocess.check_output(cmd).strip()
458+
459+
460+def symlink(source, destination):
461+ """Create a symbolic link"""
462+ context = execution_environment()
463+ log("Symlinking {} as {}".format(source, destination))
464+ cmd = [
465+ 'ln',
466+ '-sf',
467+ source.format(**context),
468+ destination.format(**context)
469+ ]
470+ subprocess.check_call(cmd)
471+
472+
473+def mkdir(path, owner='root', group='root', perms=0555, force=False):
474+ """Create a directory"""
475+ context = execution_environment()
476+ log("Making dir {} {}:{} {:o}".format(path, owner, group,
477+ perms))
478+ uid = pwd.getpwnam(owner.format(**context)).pw_uid
479+ gid = grp.getgrnam(group.format(**context)).gr_gid
480+ realpath = os.path.abspath(path)
481+ if os.path.exists(realpath):
482+ if force and not os.path.isdir(realpath):
483+ log("Removing non-directory file {} prior to mkdir()".format(path))
484+ os.unlink(realpath)
485+ else:
486+ os.makedirs(realpath, perms)
487+ os.chown(realpath, uid, gid)
488+
489+
490+def write_file(path, content, owner='root', group='root', perms=0444):
491+ """Create or overwrite a file with the contents of a string"""
492+ log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
493+ uid = pwd.getpwnam(owner).pw_uid
494+ gid = grp.getgrnam(group).gr_gid
495+ with open(path, 'w') as target:
496+ os.fchown(target.fileno(), uid, gid)
497+ os.fchmod(target.fileno(), perms)
498+ target.write(content)
499+
500+
501+def filter_installed_packages(packages):
502+ """Returns a list of packages that require installation"""
503+ apt_pkg.init()
504+ cache = apt_pkg.Cache()
505+ _pkgs = []
506+ for package in packages:
507+ try:
508+ p = cache[package]
509+ p.current_ver or _pkgs.append(package)
510+ except KeyError:
511+ log('Package {} has no installation candidate.'.format(package),
512+ level='WARNING')
513+ _pkgs.append(package)
514+ return _pkgs
515+
516+
517+def apt_install(packages, options=None, fatal=False):
518+ """Install one or more packages"""
519+ options = options or []
520+ cmd = ['apt-get', '-y']
521+ cmd.extend(options)
522+ cmd.append('install')
523+ if isinstance(packages, basestring):
524+ cmd.append(packages)
525+ else:
526+ cmd.extend(packages)
527+ log("Installing {} with options: {}".format(packages,
528+ options))
529+ if fatal:
530+ subprocess.check_call(cmd)
531+ else:
532+ subprocess.call(cmd)
533+
534+
535+def apt_update(fatal=False):
536+ """Update local apt cache"""
537+ cmd = ['apt-get', 'update']
538+ if fatal:
539+ subprocess.check_call(cmd)
540+ else:
541+ subprocess.call(cmd)
542+
543+
544+def mount(device, mountpoint, options=None, persist=False):
545+ '''Mount a filesystem'''
546+ cmd_args = ['mount']
547+ if options is not None:
548+ cmd_args.extend(['-o', options])
549+ cmd_args.extend([device, mountpoint])
550+ try:
551+ subprocess.check_output(cmd_args)
552+ except subprocess.CalledProcessError, e:
553+ log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
554+ return False
555+ if persist:
556+ # TODO: update fstab
557+ pass
558+ return True
559+
560+
561+def umount(mountpoint, persist=False):
562+ '''Unmount a filesystem'''
563+ cmd_args = ['umount', mountpoint]
564+ try:
565+ subprocess.check_output(cmd_args)
566+ except subprocess.CalledProcessError, e:
567+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
568+ return False
569+ if persist:
570+ # TODO: update fstab
571+ pass
572+ return True
573+
574+
575+def mounts():
576+ '''List of all mounted volumes as [[mountpoint,device],[...]]'''
577+ with open('/proc/mounts') as f:
578+ # [['/mount/point','/dev/path'],[...]]
579+ system_mounts = [m[1::-1] for m in [l.strip().split()
580+ for l in f.readlines()]]
581+ return system_mounts
582+
583+
584+def file_hash(path):
585+ ''' Generate a md5 hash of the contents of 'path' or None if not found '''
586+ if os.path.exists(path):
587+ h = hashlib.md5()
588+ with open(path, 'r') as source:
589+ h.update(source.read()) # IGNORE:E1101 - it does have update
590+ return h.hexdigest()
591+ else:
592+ return None
593+
594+
595+def restart_on_change(restart_map):
596+ ''' Restart services based on configuration files changing
597+
598+ This function is used a decorator, for example
599+
600+ @restart_on_change({
601+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
602+ })
603+ def ceph_client_changed():
604+ ...
605+
606+ In this example, the cinder-api and cinder-volume services
607+ would be restarted if /etc/ceph/ceph.conf is changed by the
608+ ceph_client_changed function.
609+ '''
610+ def wrap(f):
611+ def wrapped_f(*args):
612+ checksums = {}
613+ for path in restart_map:
614+ checksums[path] = file_hash(path)
615+ f(*args)
616+ restarts = []
617+ for path in restart_map:
618+ if checksums[path] != file_hash(path):
619+ restarts += restart_map[path]
620+ for service_name in list(OrderedDict.fromkeys(restarts)):
621+ service('restart', service_name)
622+ return wrapped_f
623+ return wrap
624+
625+
626+def lsb_release():
627+ '''Return /etc/lsb-release in a dict'''
628+ d = {}
629+ with open('/etc/lsb-release', 'r') as lsb:
630+ for l in lsb:
631+ k, v = l.split('=')
632+ d[k.strip()] = v.strip()
633+ return d
634
635=== modified file 'hooks/hooks.py'
636--- hooks/hooks.py 2013-05-28 09:53:30 +0000
637+++ hooks/hooks.py 2013-07-16 12:39:27 +0000
638@@ -1,89 +1,52 @@
639 #!/usr/bin/env python
640
641-from grp import getgrnam
642-import json
643 import os.path
644-from pwd import getpwnam
645 import shutil
646-import subprocess
647+import sys
648 from textwrap import dedent
649
650+from charmhelpers.core import hookenv, host
651+from charmhelpers.core.hookenv import log, DEBUG, INFO
652+
653
654 CLIENT_RELATION_TYPES = frozenset(['db', 'db-admin'])
655
656
657-def relation_ids(relation_types):
658- relids = []
659- for reltype in relation_types:
660- relid_cmd_line = ['relation-ids', '--format=json', reltype]
661- json_relids = subprocess.check_output(relid_cmd_line).strip()
662- if json_relids:
663- relids.extend(json.loads(json_relids))
664- return relids
665-
666-
667-def relation_list(relation_id):
668- """Return the list of units participating in the relation."""
669- cmd = ['relation-list', '--format=json', '-r', relation_id]
670- json_units = subprocess.check_output(cmd).strip()
671- if json_units:
672- return json.loads(json_units)
673- return []
674-
675-
676-def relation_get(relation_id, unit):
677- cmd = ['relation-get', '--format=json', '-r', relation_id, '-', unit]
678- json_relation = subprocess.check_output(cmd)
679- if json_relation:
680- return json.loads(json_relation)
681- return {}
682-
683-
684-def relation_set(relation_id, keyvalues):
685- command = ['relation-set', '-r', relation_id]
686- command.extend(
687- ["{}={}".format(k, v or '') for k, v in keyvalues.items()])
688- subprocess.check_call(command)
689-
690-
691-def config_get():
692- command = ['config-get', '--format=json']
693- json_config = subprocess.check_output(command)
694- if json_config:
695- return json.loads(json_config)
696- return {}
697-
698-
699 def all_relations(relation_types=CLIENT_RELATION_TYPES):
700 for reltype in relation_types:
701- for relid in relation_ids(relation_types=[reltype]):
702- for unit in relation_list(relation_id=relid):
703- yield reltype, relid, unit, relation_get(relid, unit)
704-
705-
706+ for relid in hookenv.relation_ids(reltype):
707+ for unit in hookenv.related_units(relid):
708+ yield reltype, relid, unit, hookenv.relation_get(
709+ unit=unit, rid=relid)
710+
711+hooks = hookenv.Hooks()
712+
713+@hooks.hook(
714+ 'config-changed', 'upgrade-charm',
715+ 'db-relation-changed', 'db-relation-joined',
716+ 'db-admin-relation-changed', 'db-admin-relation-joined')
717 def rebuild_all_relations():
718- config = config_get()
719+ config = hookenv.config()
720
721 # Clear out old scripts and pgpass files
722 if os.path.exists('bin'):
723 shutil.rmtree('bin')
724 if os.path.exists('pgpass'):
725 shutil.rmtree('pgpass')
726- install_dir('bin', mode=0o755)
727- install_dir('pgpass', group='ubuntu', mode=0o750)
728+ host.mkdir('bin', perms=0o755)
729+ host.mkdir('pgpass', group='ubuntu', perms=0o750)
730
731 for _, relid, unit, relation in all_relations(relation_types=['db']):
732- juju_log(MSG_DEBUG, "{} {} {!r}".format(relid, unit, relation))
733+ log("{} {} {!r}".format(relid, unit, relation), DEBUG)
734 if config['database'] and relation['database'] != config['database']:
735- juju_log(
736- MSG_INFO, "Overriding generated database {} with {}".format(
737- relation['database'], config['database']))
738- relation_set(relid, dict(database=config['database']))
739+ log("Overriding generated database {} with {}".format(
740+ relation['database'], config['database']), INFO)
741+ hookenv.relation_set(relid, database=config['database'])
742 elif 'user' in relation:
743 rebuild_relation(relid, unit, relation)
744
745 for _, relid, unit, relation in all_relations(relation_types=['db-admin']):
746- juju_log(MSG_DEBUG, "{} {} {!r}".format(relid, unit, relation))
747+ log("{} {} {!r}".format(relid, unit, relation), DEBUG)
748 if 'user' in relation:
749 rebuild_relation(relid, unit, relation)
750
751@@ -91,11 +54,17 @@
752 def rebuild_relation(relid, unit, relation):
753 relname = relid.split(':')[0]
754 unitname = unit.replace('/', '-')
755+ this_unit = hookenv.local_unit()
756+
757+ allowed_units = relation.get('allowed-units', '')
758+ if this_unit not in allowed_units.split():
759+ log("Not yet authorized on {}".format(relid), INFO)
760+ return
761
762 script_name = 'psql-{}-{}'.format(relname, unitname)
763 build_script(script_name, relation)
764 state = relation.get('state', None)
765- if state and state != 'standalone':
766+ if state in ('master', 'hot standby'):
767 script_name = 'psql-{}-{}'.format(relname, state.replace(' ', '-'))
768 build_script(script_name, relation)
769
770@@ -117,45 +86,18 @@
771 database=relation.get('database', ''), # db-admin has no database
772 user=relation['user'],
773 pgpass=pgpass_path)
774- juju_log(MSG_INFO, "Generating wrapper {}".format(script_path))
775- install_file(
776- script, script_path, owner="ubuntu", group="ubuntu", mode=0o700)
777+ log("Generating wrapper {}".format(script_path), INFO)
778+ host.write_file(
779+ script_path, script, owner="ubuntu", group="ubuntu", perms=0o700)
780
781 # The wrapper requires access to the password, stored in a .pgpass
782 # file so it isn't exposed in an environment variable or on the
783 # command line.
784 pgpass = "*:*:*:{user}:{password}".format(
785 user=relation['user'], password=relation['password'])
786- install_file(
787- pgpass, pgpass_path, owner="ubuntu", group="ubuntu", mode=0o400)
788-
789-
790-def install_file(contents, dest, owner="root", group="root", mode=0o600):
791- uid = getpwnam(owner)[2]
792- gid = getgrnam(group)[2]
793- dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
794- os.fchown(dest_fd, uid, gid)
795- with os.fdopen(dest_fd, 'w') as destfile:
796- destfile.write(str(contents))
797-
798-
799-def install_dir(dirname, owner="root", group="root", mode=0o700):
800- command = [
801- '/usr/bin/install',
802- '-o', owner, '-g', group, '-m', oct(mode), '-d', dirname]
803- subprocess.check_call(command)
804-
805-
806-MSG_CRITICAL = "CRITICAL"
807-MSG_DEBUG = "DEBUG"
808-MSG_INFO = "INFO"
809-MSG_ERROR = "ERROR"
810-MSG_WARNING = "WARNING"
811-
812-
813-def juju_log(level, msg):
814- subprocess.call(['juju-log', '-l', level, msg])
815+ host.write_file(
816+ pgpass_path, pgpass, owner="ubuntu", group="ubuntu", mode=0o400)
817
818
819 if __name__ == '__main__':
820- raise SystemExit(rebuild_all_relations())
821+ hooks.execute(sys.argv)
822
823=== modified file 'icon.svg'
824--- icon.svg 2013-06-04 11:32:34 +0000
825+++ icon.svg 2013-07-16 12:39:27 +0000
826@@ -1,4 +1,6 @@
827 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
828+<!-- Created with Inkscape (http://www.inkscape.org/) -->
829+
830 <svg
831 xmlns:dc="http://purl.org/dc/elements/1.1/"
832 xmlns:cc="http://creativecommons.org/ns#"
833@@ -7,86 +9,350 @@
834 xmlns="http://www.w3.org/2000/svg"
835 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
836 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
837- width="432.071pt"
838- height="445.383pt"
839- viewBox="0 0 432.071 445.383"
840- xml:space="preserve"
841- id="svg2"
842+ width="96"
843+ height="96"
844+ id="svg6517"
845 version="1.1"
846 inkscape:version="0.48.4 r9939"
847- sodipodi:docname="icon.svg"><metadata
848- id="metadata34"><rdf:RDF><cc:Work
849- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
850- rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
851- id="defs32" /><sodipodi:namedview
852+ sodipodi:docname="icon.svg">
853+ <defs
854+ id="defs6519">
855+ <linearGradient
856+ id="linearGradient3947">
857+ <stop
858+ style="stop-color:#ffffff;stop-opacity:1;"
859+ offset="0"
860+ id="stop3949" />
861+ <stop
862+ style="stop-color:#ffffff;stop-opacity:0;"
863+ offset="1"
864+ id="stop3951" />
865+ </linearGradient>
866+ <linearGradient
867+ id="Background">
868+ <stop
869+ id="stop4178"
870+ offset="0"
871+ style="stop-color:#b8b8b8;stop-opacity:1" />
872+ <stop
873+ id="stop4180"
874+ offset="1"
875+ style="stop-color:#c9c9c9;stop-opacity:1" />
876+ </linearGradient>
877+ <filter
878+ style="color-interpolation-filters:sRGB;"
879+ inkscape:label="Inner Shadow"
880+ id="filter1121">
881+ <feFlood
882+ flood-opacity="0.59999999999999998"
883+ flood-color="rgb(0,0,0)"
884+ result="flood"
885+ id="feFlood1123" />
886+ <feComposite
887+ in="flood"
888+ in2="SourceGraphic"
889+ operator="out"
890+ result="composite1"
891+ id="feComposite1125" />
892+ <feGaussianBlur
893+ in="composite1"
894+ stdDeviation="1"
895+ result="blur"
896+ id="feGaussianBlur1127" />
897+ <feOffset
898+ dx="0"
899+ dy="2"
900+ result="offset"
901+ id="feOffset1129" />
902+ <feComposite
903+ in="offset"
904+ in2="SourceGraphic"
905+ operator="atop"
906+ result="composite2"
907+ id="feComposite1131" />
908+ </filter>
909+ <filter
910+ style="color-interpolation-filters:sRGB;"
911+ inkscape:label="Drop Shadow"
912+ id="filter950">
913+ <feFlood
914+ flood-opacity="0.25"
915+ flood-color="rgb(0,0,0)"
916+ result="flood"
917+ id="feFlood952" />
918+ <feComposite
919+ in="flood"
920+ in2="SourceGraphic"
921+ operator="in"
922+ result="composite1"
923+ id="feComposite954" />
924+ <feGaussianBlur
925+ in="composite1"
926+ stdDeviation="1"
927+ result="blur"
928+ id="feGaussianBlur956" />
929+ <feOffset
930+ dx="0"
931+ dy="1"
932+ result="offset"
933+ id="feOffset958" />
934+ <feComposite
935+ in="SourceGraphic"
936+ in2="offset"
937+ operator="over"
938+ result="composite2"
939+ id="feComposite960" />
940+ </filter>
941+ <clipPath
942+ clipPathUnits="userSpaceOnUse"
943+ id="clipPath873">
944+ <g
945+ transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
946+ id="g875"
947+ inkscape:label="Layer 1"
948+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
949+ <path
950+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
951+ 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"
952+ id="path877"
953+ inkscape:connector-curvature="0"
954+ sodipodi:nodetypes="sssssssss" />
955+ </g>
956+ </clipPath>
957+ <filter
958+ inkscape:collect="always"
959+ id="filter891"
960+ inkscape:label="Badge Shadow">
961+ <feGaussianBlur
962+ inkscape:collect="always"
963+ stdDeviation="0.71999962"
964+ id="feGaussianBlur893" />
965+ </filter>
966+ </defs>
967+ <sodipodi:namedview
968+ id="base"
969 pagecolor="#ffffff"
970 bordercolor="#666666"
971- borderopacity="1"
972- objecttolerance="10"
973- gridtolerance="10"
974- guidetolerance="10"
975- inkscape:pageopacity="0"
976+ borderopacity="1.0"
977+ inkscape:pageopacity="0.0"
978 inkscape:pageshadow="2"
979- inkscape:window-width="1111"
980- inkscape:window-height="783"
981- id="namedview30"
982- showgrid="false"
983- inkscape:zoom="0.42390481"
984- inkscape:cx="270.04437"
985- inkscape:cy="278.36438"
986- inkscape:window-x="195"
987- inkscape:window-y="148"
988+ inkscape:zoom="4.0745362"
989+ inkscape:cx="49.256842"
990+ inkscape:cy="43.407288"
991+ inkscape:document-units="px"
992+ inkscape:current-layer="layer3"
993+ showgrid="true"
994+ fit-margin-top="0"
995+ fit-margin-left="0"
996+ fit-margin-right="0"
997+ fit-margin-bottom="0"
998+ inkscape:window-width="1417"
999+ inkscape:window-height="799"
1000+ inkscape:window-x="65"
1001+ inkscape:window-y="24"
1002 inkscape:window-maximized="0"
1003- inkscape:current-layer="svg2" /><g
1004- id="orginal"
1005- style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;" /><g
1006- id="Layer_x0020_3"
1007- style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;"><path
1008- style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;"
1009- d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"
1010- id="path6" /><path
1011- style="fill:#336791;stroke:none;"
1012- d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"
1013- id="path8" /><path
1014- d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"
1015- id="path10" /><path
1016- d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"
1017- id="path12" /><path
1018- d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"
1019- id="path14" /><path
1020- style="stroke-linejoin:bevel;"
1021- d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"
1022- id="path16" /><path
1023- d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"
1024- id="path18" /><path
1025- d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"
1026- id="path20" /><path
1027- style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;"
1028- d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"
1029- id="path22" /><path
1030- style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;"
1031- d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"
1032- id="path24" /><path
1033- d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"
1034- id="path26" /><path
1035- style="stroke-width:3;"
1036- d="M0,60.232"
1037- id="path28" /></g><rect
1038- style="fill:#4d4d4d;stroke:#000000;stroke-width:0.73861116"
1039- id="rect3015"
1040- width="258.81003"
1041- height="105.94549"
1042- x="-2.0179098"
1043- y="341.45541" /><text
1044- xml:space="preserve"
1045- style="font-size:68.27682495px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ff0000;fill-opacity:1;stroke:none;font-family:Courier 10 Pitch;-inkscape-font-specification:Courier 10 Pitch"
1046- x="10.404695"
1047- y="396.7569"
1048- id="text3011-6"
1049- sodipodi:linespacing="125%"
1050- transform="scale(0.96172302,1.0398004)"><tspan
1051- sodipodi:role="line"
1052- id="tspan3013-6"
1053- x="10.404695"
1054- y="396.7569">psql&gt;_</tspan></text>
1055-</svg>
1056\ No newline at end of file
1057+ showborder="true"
1058+ showguides="true"
1059+ inkscape:guide-bbox="true"
1060+ inkscape:showpageshadow="false">
1061+ <inkscape:grid
1062+ type="xygrid"
1063+ id="grid821" />
1064+ <sodipodi:guide
1065+ orientation="1,0"
1066+ position="16,48"
1067+ id="guide823" />
1068+ <sodipodi:guide
1069+ orientation="0,1"
1070+ position="64,80"
1071+ id="guide825" />
1072+ <sodipodi:guide
1073+ orientation="1,0"
1074+ position="80,40"
1075+ id="guide827" />
1076+ <sodipodi:guide
1077+ orientation="0,1"
1078+ position="64,16"
1079+ id="guide829" />
1080+ </sodipodi:namedview>
1081+ <metadata
1082+ id="metadata6522">
1083+ <rdf:RDF>
1084+ <cc:Work
1085+ rdf:about="">
1086+ <dc:format>image/svg+xml</dc:format>
1087+ <dc:type
1088+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
1089+ <dc:title></dc:title>
1090+ </cc:Work>
1091+ </rdf:RDF>
1092+ </metadata>
1093+ <g
1094+ inkscape:label="BACKGROUND"
1095+ inkscape:groupmode="layer"
1096+ id="layer1"
1097+ transform="translate(268,-635.29076)"
1098+ style="display:inline">
1099+ <path
1100+ style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121);opacity:1"
1101+ 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"
1102+ id="path6455"
1103+ inkscape:connector-curvature="0"
1104+ sodipodi:nodetypes="sssssssss" />
1105+ </g>
1106+ <g
1107+ inkscape:groupmode="layer"
1108+ id="layer3"
1109+ inkscape:label="PLACE YOUR PICTOGRAM HERE"
1110+ style="display:inline">
1111+ <g
1112+ transform="matrix(0.18231452,0,0,0.16578318,13.59438,4.5326341)"
1113+ id="Layer_x0020_3"
1114+ style="fill:none;stroke:#ffffff;stroke-width:12.46510029;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;display:inline">
1115+ <path
1116+ inkscape:connector-curvature="0"
1117+ style="fill:#000000;stroke:#000000;stroke-width:37.39530182;stroke-linecap:butt;stroke-linejoin:miter"
1118+ d="m 323.205,324.227 c 2.833,-23.601 1.984,-27.062 19.563,-23.239 l 4.463,0.392 c 13.517,0.615 31.199,-2.174 41.587,-7 22.362,-10.376 35.622,-27.7 13.572,-23.148 -50.297,10.376 -53.755,-6.655 -53.755,-6.655 C 401.746,185.774 423.948,85.741 404.784,61.255 352.514,-5.534 262.036,26.049 260.522,26.869 l -0.482,0.089 c -9.938,-2.062 -21.06,-3.294 -33.554,-3.496 -22.761,-0.374 -40.032,5.967 -53.133,15.904 0,0 -161.408,-66.498 -153.899,83.628 1.597,31.936 45.777,241.655 98.47,178.31 19.259,-23.163 37.871,-42.748 37.871,-42.748 9.242,6.14 20.307,9.272 31.912,8.147 l 0.897,-0.765 c -0.281,2.876 -0.157,5.689 0.359,9.019 -13.572,15.167 -9.584,17.83 -36.723,23.416 -27.457,5.659 -11.326,15.734 -0.797,18.367 12.768,3.193 42.305,7.716 62.268,-20.224 l -0.795,3.188 c 5.325,4.26 4.965,30.619 5.72,49.452 0.756,18.834 2.017,36.409 5.856,46.771 3.839,10.36 8.369,37.05 44.036,29.406 29.809,-6.388 52.6,-15.582 54.677,-101.107"
1119+ id="path3095" />
1120+ <path
1121+ inkscape:connector-curvature="0"
1122+ style="fill:#336791;stroke:none"
1123+ d="m 402.395,271.23 c -50.302,10.376 -53.76,-6.655 -53.76,-6.655 53.111,-78.808 75.313,-178.843 56.153,-203.326 -52.27,-66.785 -142.752,-35.2 -144.262,-34.38 l -0.486,0.087 c -9.938,-2.063 -21.06,-3.292 -33.56,-3.496 -22.761,-0.373 -40.026,5.967 -53.127,15.902 0,0 -161.411,-66.495 -153.904,83.63 1.597,31.938 45.776,241.657 98.471,178.312 19.26,-23.163 37.869,-42.748 37.869,-42.748 9.243,6.14 20.308,9.272 31.908,8.147 l 0.901,-0.765 c -0.28,2.876 -0.152,5.689 0.361,9.019 -13.575,15.167 -9.586,17.83 -36.723,23.416 -27.459,5.659 -11.328,15.734 -0.796,18.367 12.768,3.193 42.307,7.716 62.266,-20.224 l -0.796,3.188 c 5.319,4.26 9.054,27.711 8.428,48.969 -0.626,21.259 -1.044,35.854 3.147,47.254 4.191,11.4 8.368,37.05 44.042,29.406 29.809,-6.388 45.256,-22.942 47.405,-50.555 1.525,-19.631 4.976,-16.729 5.194,-34.28 l 2.768,-8.309 c 3.192,-26.611 0.507,-35.196 18.872,-31.203 l 4.463,0.392 c 13.517,0.615 31.208,-2.174 41.591,-7 22.358,-10.376 35.618,-27.7 13.573,-23.148 z"
1124+ id="path3097" />
1125+ <path
1126+ inkscape:connector-curvature="0"
1127+ d="m 215.866,286.484 c -1.385,49.516 0.348,99.377 5.193,111.495 4.848,12.118 15.223,35.688 50.9,28.045 29.806,-6.39 40.651,-18.756 45.357,-46.051 3.466,-20.082 10.148,-75.854 11.005,-87.281"
1128+ id="path3099" />
1129+ <path
1130+ inkscape:connector-curvature="0"
1131+ d="m 173.104,38.256 c 0,0 -161.521,-66.016 -154.012,84.109 1.597,31.938 45.779,241.664 98.473,178.316 19.256,-23.166 36.671,-41.335 36.671,-41.335"
1132+ id="path3101" />
1133+ <path
1134+ inkscape:connector-curvature="0"
1135+ d="m 260.349,26.207 c -5.591,1.753 89.848,-34.889 144.087,34.417 19.159,24.484 -3.043,124.519 -56.153,203.329"
1136+ id="path3103" />
1137+ <path
1138+ inkscape:connector-curvature="0"
1139+ style="stroke-linejoin:bevel"
1140+ d="m 348.282,263.953 c 0,0 3.461,17.036 53.764,6.653 22.04,-4.552 8.776,12.774 -13.577,23.155 -18.345,8.514 -59.474,10.696 -60.146,-1.069 -1.729,-30.355 21.647,-21.133 19.96,-28.739 -1.525,-6.85 -11.979,-13.573 -18.894,-30.338 -6.037,-14.633 -82.796,-126.849 21.287,-110.183 3.813,-0.789 -27.146,-99.002 -124.553,-100.599 -97.385,-1.597 -94.19,119.762 -94.19,119.762"
1141+ id="path3105" />
1142+ <path
1143+ inkscape:connector-curvature="0"
1144+ d="m 188.604,274.334 c -13.577,15.166 -9.584,17.829 -36.723,23.417 -27.459,5.66 -11.326,15.733 -0.797,18.365 12.768,3.195 42.307,7.718 62.266,-20.229 6.078,-8.509 -0.036,-22.086 -8.385,-25.547 -4.034,-1.671 -9.428,-3.765 -16.361,3.994 z"
1145+ id="path3107" />
1146+ <path
1147+ inkscape:connector-curvature="0"
1148+ d="m 187.715,274.069 c -1.368,-8.917 2.93,-19.528 7.536,-31.942 6.922,-18.626 22.893,-37.255 10.117,-96.339 -9.523,-44.029 -73.396,-9.163 -73.436,-3.193 -0.039,5.968 2.889,30.26 -1.067,58.548 -5.162,36.913 23.488,68.132 56.479,64.938"
1149+ id="path3109" />
1150+ <path
1151+ inkscape:connector-curvature="0"
1152+ style="fill:#ffffff;stroke-width:4.15500021;stroke-linecap:butt;stroke-linejoin:miter"
1153+ d="m 172.517,141.7 c -0.288,2.039 3.733,7.48 8.976,8.207 5.234,0.73 9.714,-3.522 9.998,-5.559 0.284,-2.039 -3.732,-4.285 -8.977,-5.015 -5.237,-0.731 -9.719,0.333 -9.996,2.367 z"
1154+ id="path3111" />
1155+ <path
1156+ inkscape:connector-curvature="0"
1157+ style="fill:#ffffff;stroke-width:2.0775001;stroke-linecap:butt;stroke-linejoin:miter"
1158+ d="m 331.941,137.543 c 0.284,2.039 -3.732,7.48 -8.976,8.207 -5.238,0.73 -9.718,-3.522 -10.005,-5.559 -0.277,-2.039 3.74,-4.285 8.979,-5.015 5.239,-0.73 9.718,0.333 10.002,2.368 z"
1159+ id="path3113" />
1160+ <path
1161+ inkscape:connector-curvature="0"
1162+ d="m 350.676,123.432 c 0.863,15.994 -3.445,26.888 -3.988,43.914 -0.804,24.748 11.799,53.074 -7.191,81.435"
1163+ id="path3115" />
1164+ <path
1165+ inkscape:connector-curvature="0"
1166+ style="stroke-width:3"
1167+ d="M 0,60.232"
1168+ id="path3117" />
1169+ </g>
1170+ <rect
1171+ style="fill:#4d4d4d;stroke:#000000;stroke-width:0.12356114"
1172+ id="rect3015"
1173+ width="40.634895"
1174+ height="18.884109"
1175+ x="8.1120977"
1176+ y="67.328201" />
1177+ <text
1178+ xml:space="preserve"
1179+ style="font-size:11.4219265px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ff0000;fill-opacity:1;stroke:none;font-family:Courier 10 Pitch;-inkscape-font-specification:Courier 10 Pitch"
1180+ x="11.078934"
1181+ y="72.209114"
1182+ id="text3011-6"
1183+ sodipodi:linespacing="125%"
1184+ transform="scale(0.9026139,1.1078934)"><tspan
1185+ sodipodi:role="line"
1186+ id="tspan3013-6"
1187+ x="11.078934"
1188+ y="72.209114">psql&gt;_</tspan></text>
1189+ </g>
1190+ <g
1191+ inkscape:groupmode="layer"
1192+ id="layer2"
1193+ inkscape:label="BADGE"
1194+ style="display:none"
1195+ sodipodi:insensitive="true">
1196+ <g
1197+ style="display:inline"
1198+ transform="translate(-340.00001,-581)"
1199+ id="g4394"
1200+ clip-path="none">
1201+ <g
1202+ id="g855">
1203+ <g
1204+ inkscape:groupmode="maskhelper"
1205+ id="g870"
1206+ clip-path="url(#clipPath873)"
1207+ style="opacity:0.6;filter:url(#filter891)">
1208+ <path
1209+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
1210+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
1211+ sodipodi:ry="12"
1212+ sodipodi:rx="12"
1213+ sodipodi:cy="552.36218"
1214+ sodipodi:cx="252"
1215+ id="path844"
1216+ 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"
1217+ sodipodi:type="arc" />
1218+ </g>
1219+ <g
1220+ id="g862">
1221+ <path
1222+ sodipodi:type="arc"
1223+ 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"
1224+ id="path4398"
1225+ sodipodi:cx="252"
1226+ sodipodi:cy="552.36218"
1227+ sodipodi:rx="12"
1228+ sodipodi:ry="12"
1229+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
1230+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
1231+ <path
1232+ transform="matrix(1.25,0,0,1.25,33,-100.45273)"
1233+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
1234+ sodipodi:ry="12"
1235+ sodipodi:rx="12"
1236+ sodipodi:cy="552.36218"
1237+ sodipodi:cx="252"
1238+ id="path4400"
1239+ 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"
1240+ sodipodi:type="arc" />
1241+ <path
1242+ sodipodi:type="star"
1243+ 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"
1244+ id="path4459"
1245+ sodipodi:sides="5"
1246+ sodipodi:cx="666.19574"
1247+ sodipodi:cy="589.50385"
1248+ sodipodi:r1="7.2431178"
1249+ sodipodi:r2="4.3458705"
1250+ sodipodi:arg1="1.0471976"
1251+ sodipodi:arg2="1.6755161"
1252+ inkscape:flatsided="false"
1253+ inkscape:rounded="0.1"
1254+ inkscape:randomized="0"
1255+ 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"
1256+ transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
1257+ </g>
1258+ </g>
1259+ </g>
1260+ </g>
1261+</svg>

Subscribers

People subscribed via source and target branches