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
=== added file 'charm-helpers.yaml'
--- charm-helpers.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers.yaml 2013-07-16 12:39:27 +0000
@@ -0,0 +1,4 @@
1destination: hooks/charmhelpers
2branch: lp:charm-helpers
3include:
4 - core
05
=== added directory 'hooks/charmhelpers'
=== added file 'hooks/charmhelpers/__init__.py'
=== added directory 'hooks/charmhelpers/core'
=== added file 'hooks/charmhelpers/core/__init__.py'
=== added file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/hookenv.py 2013-07-16 12:39:27 +0000
@@ -0,0 +1,339 @@
1"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.
3#
4# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>
6
7import os
8import json
9import yaml
10import subprocess
11import UserDict
12
13CRITICAL = "CRITICAL"
14ERROR = "ERROR"
15WARNING = "WARNING"
16INFO = "INFO"
17DEBUG = "DEBUG"
18MARKER = object()
19
20cache = {}
21
22
23def cached(func):
24 ''' Cache return values for multiple executions of func + args
25
26 For example:
27
28 @cached
29 def unit_get(attribute):
30 pass
31
32 unit_get('test')
33
34 will cache the result of unit_get + 'test' for future calls.
35 '''
36 def wrapper(*args, **kwargs):
37 global cache
38 key = str((func, args, kwargs))
39 try:
40 return cache[key]
41 except KeyError:
42 res = func(*args, **kwargs)
43 cache[key] = res
44 return res
45 return wrapper
46
47
48def flush(key):
49 ''' Flushes any entries from function cache where the
50 key is found in the function+args '''
51 flush_list = []
52 for item in cache:
53 if key in item:
54 flush_list.append(item)
55 for item in flush_list:
56 del cache[item]
57
58
59def log(message, level=None):
60 "Write a message to the juju log"
61 command = ['juju-log']
62 if level:
63 command += ['-l', level]
64 command += [message]
65 subprocess.call(command)
66
67
68class Serializable(UserDict.IterableUserDict):
69 "Wrapper, an object that can be serialized to yaml or json"
70
71 def __init__(self, obj):
72 # wrap the object
73 UserDict.IterableUserDict.__init__(self)
74 self.data = obj
75
76 def __getattr__(self, attr):
77 # See if this object has attribute.
78 if attr in ("json", "yaml", "data"):
79 return self.__dict__[attr]
80 # Check for attribute in wrapped object.
81 got = getattr(self.data, attr, MARKER)
82 if got is not MARKER:
83 return got
84 # Proxy to the wrapped object via dict interface.
85 try:
86 return self.data[attr]
87 except KeyError:
88 raise AttributeError(attr)
89
90 def __getstate__(self):
91 # Pickle as a standard dictionary.
92 return self.data
93
94 def __setstate__(self, state):
95 # Unpickle into our wrapper.
96 self.data = state
97
98 def json(self):
99 "Serialize the object to json"
100 return json.dumps(self.data)
101
102 def yaml(self):
103 "Serialize the object to yaml"
104 return yaml.dump(self.data)
105
106
107def execution_environment():
108 """A convenient bundling of the current execution context"""
109 context = {}
110 context['conf'] = config()
111 if relation_id():
112 context['reltype'] = relation_type()
113 context['relid'] = relation_id()
114 context['rel'] = relation_get()
115 context['unit'] = local_unit()
116 context['rels'] = relations()
117 context['env'] = os.environ
118 return context
119
120
121def in_relation_hook():
122 "Determine whether we're running in a relation hook"
123 return 'JUJU_RELATION' in os.environ
124
125
126def relation_type():
127 "The scope for the current relation hook"
128 return os.environ.get('JUJU_RELATION', None)
129
130
131def relation_id():
132 "The relation ID for the current relation hook"
133 return os.environ.get('JUJU_RELATION_ID', None)
134
135
136def local_unit():
137 "Local unit ID"
138 return os.environ['JUJU_UNIT_NAME']
139
140
141def remote_unit():
142 "The remote unit for the current relation hook"
143 return os.environ['JUJU_REMOTE_UNIT']
144
145
146def service_name():
147 "The name service group this unit belongs to"
148 return local_unit().split('/')[0]
149
150
151@cached
152def config(scope=None):
153 "Juju charm configuration"
154 config_cmd_line = ['config-get']
155 if scope is not None:
156 config_cmd_line.append(scope)
157 config_cmd_line.append('--format=json')
158 try:
159 return json.loads(subprocess.check_output(config_cmd_line))
160 except ValueError:
161 return None
162
163
164@cached
165def relation_get(attribute=None, unit=None, rid=None):
166 _args = ['relation-get', '--format=json']
167 if rid:
168 _args.append('-r')
169 _args.append(rid)
170 _args.append(attribute or '-')
171 if unit:
172 _args.append(unit)
173 try:
174 return json.loads(subprocess.check_output(_args))
175 except ValueError:
176 return None
177
178
179def relation_set(relation_id=None, relation_settings={}, **kwargs):
180 relation_cmd_line = ['relation-set']
181 if relation_id is not None:
182 relation_cmd_line.extend(('-r', relation_id))
183 for k, v in (relation_settings.items() + kwargs.items()):
184 if v is None:
185 relation_cmd_line.append('{}='.format(k))
186 else:
187 relation_cmd_line.append('{}={}'.format(k, v))
188 subprocess.check_call(relation_cmd_line)
189 # Flush cache of any relation-gets for local unit
190 flush(local_unit())
191
192
193@cached
194def relation_ids(reltype=None):
195 "A list of relation_ids"
196 reltype = reltype or relation_type()
197 relid_cmd_line = ['relation-ids', '--format=json']
198 if reltype is not None:
199 relid_cmd_line.append(reltype)
200 return json.loads(subprocess.check_output(relid_cmd_line)) or []
201 return []
202
203
204@cached
205def related_units(relid=None):
206 "A list of related units"
207 relid = relid or relation_id()
208 units_cmd_line = ['relation-list', '--format=json']
209 if relid is not None:
210 units_cmd_line.extend(('-r', relid))
211 return json.loads(subprocess.check_output(units_cmd_line)) or []
212
213
214@cached
215def relation_for_unit(unit=None, rid=None):
216 "Get the json represenation of a unit's relation"
217 unit = unit or remote_unit()
218 relation = relation_get(unit=unit, rid=rid)
219 for key in relation:
220 if key.endswith('-list'):
221 relation[key] = relation[key].split()
222 relation['__unit__'] = unit
223 return relation
224
225
226@cached
227def relations_for_id(relid=None):
228 "Get relations of a specific relation ID"
229 relation_data = []
230 relid = relid or relation_ids()
231 for unit in related_units(relid):
232 unit_data = relation_for_unit(unit, relid)
233 unit_data['__relid__'] = relid
234 relation_data.append(unit_data)
235 return relation_data
236
237
238@cached
239def relations_of_type(reltype=None):
240 "Get relations of a specific type"
241 relation_data = []
242 reltype = reltype or relation_type()
243 for relid in relation_ids(reltype):
244 for relation in relations_for_id(relid):
245 relation['__relid__'] = relid
246 relation_data.append(relation)
247 return relation_data
248
249
250@cached
251def relation_types():
252 "Get a list of relation types supported by this charm"
253 charmdir = os.environ.get('CHARM_DIR', '')
254 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
255 md = yaml.safe_load(mdf)
256 rel_types = []
257 for key in ('provides', 'requires', 'peers'):
258 section = md.get(key)
259 if section:
260 rel_types.extend(section.keys())
261 mdf.close()
262 return rel_types
263
264
265@cached
266def relations():
267 rels = {}
268 for reltype in relation_types():
269 relids = {}
270 for relid in relation_ids(reltype):
271 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
272 for unit in related_units(relid):
273 reldata = relation_get(unit=unit, rid=relid)
274 units[unit] = reldata
275 relids[relid] = units
276 rels[reltype] = relids
277 return rels
278
279
280def open_port(port, protocol="TCP"):
281 "Open a service network port"
282 _args = ['open-port']
283 _args.append('{}/{}'.format(port, protocol))
284 subprocess.check_call(_args)
285
286
287def close_port(port, protocol="TCP"):
288 "Close a service network port"
289 _args = ['close-port']
290 _args.append('{}/{}'.format(port, protocol))
291 subprocess.check_call(_args)
292
293
294@cached
295def unit_get(attribute):
296 _args = ['unit-get', '--format=json', attribute]
297 try:
298 return json.loads(subprocess.check_output(_args))
299 except ValueError:
300 return None
301
302
303def unit_private_ip():
304 return unit_get('private-address')
305
306
307class UnregisteredHookError(Exception):
308 pass
309
310
311class Hooks(object):
312 def __init__(self):
313 super(Hooks, self).__init__()
314 self._hooks = {}
315
316 def register(self, name, function):
317 self._hooks[name] = function
318
319 def execute(self, args):
320 hook_name = os.path.basename(args[0])
321 if hook_name in self._hooks:
322 self._hooks[hook_name]()
323 else:
324 raise UnregisteredHookError(hook_name)
325
326 def hook(self, *hook_names):
327 def wrapper(decorated):
328 for hook_name in hook_names:
329 self.register(hook_name, decorated)
330 else:
331 self.register(decorated.__name__, decorated)
332 if '_' in decorated.__name__:
333 self.register(
334 decorated.__name__.replace('_', '-'), decorated)
335 return decorated
336 return wrapper
337
338def charm_dir():
339 return os.environ.get('CHARM_DIR')
0340
=== added file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/host.py 2013-07-16 12:39:27 +0000
@@ -0,0 +1,272 @@
1"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.
3#
4# Authors:
5# Nick Moffitt <nick.moffitt@canonical.com>
6# Matthew Wedgwood <matthew.wedgwood@canonical.com>
7
8import apt_pkg
9import os
10import pwd
11import grp
12import subprocess
13import hashlib
14
15from collections import OrderedDict
16
17from hookenv import log, execution_environment
18
19
20def service_start(service_name):
21 service('start', service_name)
22
23
24def service_stop(service_name):
25 service('stop', service_name)
26
27
28def service_restart(service_name):
29 service('restart', service_name)
30
31
32def service_reload(service_name, restart_on_failure=False):
33 if not service('reload', service_name) and restart_on_failure:
34 service('restart', service_name)
35
36
37def service(action, service_name):
38 cmd = ['service', service_name, action]
39 return subprocess.call(cmd) == 0
40
41
42def service_running(service):
43 try:
44 output = subprocess.check_output(['service', service, 'status'])
45 except subprocess.CalledProcessError:
46 return False
47 else:
48 if ("start/running" in output or "is running" in output):
49 return True
50 else:
51 return False
52
53
54def adduser(username, password=None, shell='/bin/bash', system_user=False):
55 """Add a user"""
56 try:
57 user_info = pwd.getpwnam(username)
58 log('user {0} already exists!'.format(username))
59 except KeyError:
60 log('creating user {0}'.format(username))
61 cmd = ['useradd']
62 if system_user or password is None:
63 cmd.append('--system')
64 else:
65 cmd.extend([
66 '--create-home',
67 '--shell', shell,
68 '--password', password,
69 ])
70 cmd.append(username)
71 subprocess.check_call(cmd)
72 user_info = pwd.getpwnam(username)
73 return user_info
74
75
76def add_user_to_group(username, group):
77 """Add a user to a group"""
78 cmd = [
79 'gpasswd', '-a',
80 username,
81 group
82 ]
83 log("Adding user {} to group {}".format(username, group))
84 subprocess.check_call(cmd)
85
86
87def rsync(from_path, to_path, flags='-r', options=None):
88 """Replicate the contents of a path"""
89 context = execution_environment()
90 options = options or ['--delete', '--executability']
91 cmd = ['/usr/bin/rsync', flags]
92 cmd.extend(options)
93 cmd.append(from_path.format(**context))
94 cmd.append(to_path.format(**context))
95 log(" ".join(cmd))
96 return subprocess.check_output(cmd).strip()
97
98
99def symlink(source, destination):
100 """Create a symbolic link"""
101 context = execution_environment()
102 log("Symlinking {} as {}".format(source, destination))
103 cmd = [
104 'ln',
105 '-sf',
106 source.format(**context),
107 destination.format(**context)
108 ]
109 subprocess.check_call(cmd)
110
111
112def mkdir(path, owner='root', group='root', perms=0555, force=False):
113 """Create a directory"""
114 context = execution_environment()
115 log("Making dir {} {}:{} {:o}".format(path, owner, group,
116 perms))
117 uid = pwd.getpwnam(owner.format(**context)).pw_uid
118 gid = grp.getgrnam(group.format(**context)).gr_gid
119 realpath = os.path.abspath(path)
120 if os.path.exists(realpath):
121 if force and not os.path.isdir(realpath):
122 log("Removing non-directory file {} prior to mkdir()".format(path))
123 os.unlink(realpath)
124 else:
125 os.makedirs(realpath, perms)
126 os.chown(realpath, uid, gid)
127
128
129def write_file(path, content, owner='root', group='root', perms=0444):
130 """Create or overwrite a file with the contents of a string"""
131 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
132 uid = pwd.getpwnam(owner).pw_uid
133 gid = grp.getgrnam(group).gr_gid
134 with open(path, 'w') as target:
135 os.fchown(target.fileno(), uid, gid)
136 os.fchmod(target.fileno(), perms)
137 target.write(content)
138
139
140def filter_installed_packages(packages):
141 """Returns a list of packages that require installation"""
142 apt_pkg.init()
143 cache = apt_pkg.Cache()
144 _pkgs = []
145 for package in packages:
146 try:
147 p = cache[package]
148 p.current_ver or _pkgs.append(package)
149 except KeyError:
150 log('Package {} has no installation candidate.'.format(package),
151 level='WARNING')
152 _pkgs.append(package)
153 return _pkgs
154
155
156def apt_install(packages, options=None, fatal=False):
157 """Install one or more packages"""
158 options = options or []
159 cmd = ['apt-get', '-y']
160 cmd.extend(options)
161 cmd.append('install')
162 if isinstance(packages, basestring):
163 cmd.append(packages)
164 else:
165 cmd.extend(packages)
166 log("Installing {} with options: {}".format(packages,
167 options))
168 if fatal:
169 subprocess.check_call(cmd)
170 else:
171 subprocess.call(cmd)
172
173
174def apt_update(fatal=False):
175 """Update local apt cache"""
176 cmd = ['apt-get', 'update']
177 if fatal:
178 subprocess.check_call(cmd)
179 else:
180 subprocess.call(cmd)
181
182
183def mount(device, mountpoint, options=None, persist=False):
184 '''Mount a filesystem'''
185 cmd_args = ['mount']
186 if options is not None:
187 cmd_args.extend(['-o', options])
188 cmd_args.extend([device, mountpoint])
189 try:
190 subprocess.check_output(cmd_args)
191 except subprocess.CalledProcessError, e:
192 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
193 return False
194 if persist:
195 # TODO: update fstab
196 pass
197 return True
198
199
200def umount(mountpoint, persist=False):
201 '''Unmount a filesystem'''
202 cmd_args = ['umount', mountpoint]
203 try:
204 subprocess.check_output(cmd_args)
205 except subprocess.CalledProcessError, e:
206 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
207 return False
208 if persist:
209 # TODO: update fstab
210 pass
211 return True
212
213
214def mounts():
215 '''List of all mounted volumes as [[mountpoint,device],[...]]'''
216 with open('/proc/mounts') as f:
217 # [['/mount/point','/dev/path'],[...]]
218 system_mounts = [m[1::-1] for m in [l.strip().split()
219 for l in f.readlines()]]
220 return system_mounts
221
222
223def file_hash(path):
224 ''' Generate a md5 hash of the contents of 'path' or None if not found '''
225 if os.path.exists(path):
226 h = hashlib.md5()
227 with open(path, 'r') as source:
228 h.update(source.read()) # IGNORE:E1101 - it does have update
229 return h.hexdigest()
230 else:
231 return None
232
233
234def restart_on_change(restart_map):
235 ''' Restart services based on configuration files changing
236
237 This function is used a decorator, for example
238
239 @restart_on_change({
240 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
241 })
242 def ceph_client_changed():
243 ...
244
245 In this example, the cinder-api and cinder-volume services
246 would be restarted if /etc/ceph/ceph.conf is changed by the
247 ceph_client_changed function.
248 '''
249 def wrap(f):
250 def wrapped_f(*args):
251 checksums = {}
252 for path in restart_map:
253 checksums[path] = file_hash(path)
254 f(*args)
255 restarts = []
256 for path in restart_map:
257 if checksums[path] != file_hash(path):
258 restarts += restart_map[path]
259 for service_name in list(OrderedDict.fromkeys(restarts)):
260 service('restart', service_name)
261 return wrapped_f
262 return wrap
263
264
265def lsb_release():
266 '''Return /etc/lsb-release in a dict'''
267 d = {}
268 with open('/etc/lsb-release', 'r') as lsb:
269 for l in lsb:
270 k, v = l.split('=')
271 d[k.strip()] = v.strip()
272 return d
0273
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2013-05-28 09:53:30 +0000
+++ hooks/hooks.py 2013-07-16 12:39:27 +0000
@@ -1,89 +1,52 @@
1#!/usr/bin/env python1#!/usr/bin/env python
22
3from grp import getgrnam
4import json
5import os.path3import os.path
6from pwd import getpwnam
7import shutil4import shutil
8import subprocess5import sys
9from textwrap import dedent6from textwrap import dedent
107
8from charmhelpers.core import hookenv, host
9from charmhelpers.core.hookenv import log, DEBUG, INFO
10
1111
12CLIENT_RELATION_TYPES = frozenset(['db', 'db-admin'])12CLIENT_RELATION_TYPES = frozenset(['db', 'db-admin'])
1313
1414
15def relation_ids(relation_types):
16 relids = []
17 for reltype in relation_types:
18 relid_cmd_line = ['relation-ids', '--format=json', reltype]
19 json_relids = subprocess.check_output(relid_cmd_line).strip()
20 if json_relids:
21 relids.extend(json.loads(json_relids))
22 return relids
23
24
25def relation_list(relation_id):
26 """Return the list of units participating in the relation."""
27 cmd = ['relation-list', '--format=json', '-r', relation_id]
28 json_units = subprocess.check_output(cmd).strip()
29 if json_units:
30 return json.loads(json_units)
31 return []
32
33
34def relation_get(relation_id, unit):
35 cmd = ['relation-get', '--format=json', '-r', relation_id, '-', unit]
36 json_relation = subprocess.check_output(cmd)
37 if json_relation:
38 return json.loads(json_relation)
39 return {}
40
41
42def relation_set(relation_id, keyvalues):
43 command = ['relation-set', '-r', relation_id]
44 command.extend(
45 ["{}={}".format(k, v or '') for k, v in keyvalues.items()])
46 subprocess.check_call(command)
47
48
49def config_get():
50 command = ['config-get', '--format=json']
51 json_config = subprocess.check_output(command)
52 if json_config:
53 return json.loads(json_config)
54 return {}
55
56
57def all_relations(relation_types=CLIENT_RELATION_TYPES):15def all_relations(relation_types=CLIENT_RELATION_TYPES):
58 for reltype in relation_types:16 for reltype in relation_types:
59 for relid in relation_ids(relation_types=[reltype]):17 for relid in hookenv.relation_ids(reltype):
60 for unit in relation_list(relation_id=relid):18 for unit in hookenv.related_units(relid):
61 yield reltype, relid, unit, relation_get(relid, unit)19 yield reltype, relid, unit, hookenv.relation_get(
6220 unit=unit, rid=relid)
6321
22hooks = hookenv.Hooks()
23
24@hooks.hook(
25 'config-changed', 'upgrade-charm',
26 'db-relation-changed', 'db-relation-joined',
27 'db-admin-relation-changed', 'db-admin-relation-joined')
64def rebuild_all_relations():28def rebuild_all_relations():
65 config = config_get()29 config = hookenv.config()
6630
67 # Clear out old scripts and pgpass files31 # Clear out old scripts and pgpass files
68 if os.path.exists('bin'):32 if os.path.exists('bin'):
69 shutil.rmtree('bin')33 shutil.rmtree('bin')
70 if os.path.exists('pgpass'):34 if os.path.exists('pgpass'):
71 shutil.rmtree('pgpass')35 shutil.rmtree('pgpass')
72 install_dir('bin', mode=0o755)36 host.mkdir('bin', perms=0o755)
73 install_dir('pgpass', group='ubuntu', mode=0o750)37 host.mkdir('pgpass', group='ubuntu', perms=0o750)
7438
75 for _, relid, unit, relation in all_relations(relation_types=['db']):39 for _, relid, unit, relation in all_relations(relation_types=['db']):
76 juju_log(MSG_DEBUG, "{} {} {!r}".format(relid, unit, relation))40 log("{} {} {!r}".format(relid, unit, relation), DEBUG)
77 if config['database'] and relation['database'] != config['database']:41 if config['database'] and relation['database'] != config['database']:
78 juju_log(42 log("Overriding generated database {} with {}".format(
79 MSG_INFO, "Overriding generated database {} with {}".format(43 relation['database'], config['database']), INFO)
80 relation['database'], config['database']))44 hookenv.relation_set(relid, database=config['database'])
81 relation_set(relid, dict(database=config['database']))
82 elif 'user' in relation:45 elif 'user' in relation:
83 rebuild_relation(relid, unit, relation)46 rebuild_relation(relid, unit, relation)
8447
85 for _, relid, unit, relation in all_relations(relation_types=['db-admin']):48 for _, relid, unit, relation in all_relations(relation_types=['db-admin']):
86 juju_log(MSG_DEBUG, "{} {} {!r}".format(relid, unit, relation))49 log("{} {} {!r}".format(relid, unit, relation), DEBUG)
87 if 'user' in relation:50 if 'user' in relation:
88 rebuild_relation(relid, unit, relation)51 rebuild_relation(relid, unit, relation)
8952
@@ -91,11 +54,17 @@
91def rebuild_relation(relid, unit, relation):54def rebuild_relation(relid, unit, relation):
92 relname = relid.split(':')[0]55 relname = relid.split(':')[0]
93 unitname = unit.replace('/', '-')56 unitname = unit.replace('/', '-')
57 this_unit = hookenv.local_unit()
58
59 allowed_units = relation.get('allowed-units', '')
60 if this_unit not in allowed_units.split():
61 log("Not yet authorized on {}".format(relid), INFO)
62 return
9463
95 script_name = 'psql-{}-{}'.format(relname, unitname)64 script_name = 'psql-{}-{}'.format(relname, unitname)
96 build_script(script_name, relation)65 build_script(script_name, relation)
97 state = relation.get('state', None)66 state = relation.get('state', None)
98 if state and state != 'standalone':67 if state in ('master', 'hot standby'):
99 script_name = 'psql-{}-{}'.format(relname, state.replace(' ', '-'))68 script_name = 'psql-{}-{}'.format(relname, state.replace(' ', '-'))
100 build_script(script_name, relation)69 build_script(script_name, relation)
10170
@@ -117,45 +86,18 @@
117 database=relation.get('database', ''), # db-admin has no database86 database=relation.get('database', ''), # db-admin has no database
118 user=relation['user'],87 user=relation['user'],
119 pgpass=pgpass_path)88 pgpass=pgpass_path)
120 juju_log(MSG_INFO, "Generating wrapper {}".format(script_path))89 log("Generating wrapper {}".format(script_path), INFO)
121 install_file(90 host.write_file(
122 script, script_path, owner="ubuntu", group="ubuntu", mode=0o700)91 script_path, script, owner="ubuntu", group="ubuntu", perms=0o700)
12392
124 # The wrapper requires access to the password, stored in a .pgpass93 # The wrapper requires access to the password, stored in a .pgpass
125 # file so it isn't exposed in an environment variable or on the94 # file so it isn't exposed in an environment variable or on the
126 # command line.95 # command line.
127 pgpass = "*:*:*:{user}:{password}".format(96 pgpass = "*:*:*:{user}:{password}".format(
128 user=relation['user'], password=relation['password'])97 user=relation['user'], password=relation['password'])
129 install_file(98 host.write_file(
130 pgpass, pgpass_path, owner="ubuntu", group="ubuntu", mode=0o400)99 pgpass_path, pgpass, owner="ubuntu", group="ubuntu", mode=0o400)
131
132
133def install_file(contents, dest, owner="root", group="root", mode=0o600):
134 uid = getpwnam(owner)[2]
135 gid = getgrnam(group)[2]
136 dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
137 os.fchown(dest_fd, uid, gid)
138 with os.fdopen(dest_fd, 'w') as destfile:
139 destfile.write(str(contents))
140
141
142def install_dir(dirname, owner="root", group="root", mode=0o700):
143 command = [
144 '/usr/bin/install',
145 '-o', owner, '-g', group, '-m', oct(mode), '-d', dirname]
146 subprocess.check_call(command)
147
148
149MSG_CRITICAL = "CRITICAL"
150MSG_DEBUG = "DEBUG"
151MSG_INFO = "INFO"
152MSG_ERROR = "ERROR"
153MSG_WARNING = "WARNING"
154
155
156def juju_log(level, msg):
157 subprocess.call(['juju-log', '-l', level, msg])
158100
159101
160if __name__ == '__main__':102if __name__ == '__main__':
161 raise SystemExit(rebuild_all_relations())103 hooks.execute(sys.argv)
162104
=== modified file 'icon.svg'
--- icon.svg 2013-06-04 11:32:34 +0000
+++ icon.svg 2013-07-16 12:39:27 +0000
@@ -1,4 +1,6 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
2<svg4<svg
3 xmlns:dc="http://purl.org/dc/elements/1.1/"5 xmlns:dc="http://purl.org/dc/elements/1.1/"
4 xmlns:cc="http://creativecommons.org/ns#"6 xmlns:cc="http://creativecommons.org/ns#"
@@ -7,86 +9,350 @@
7 xmlns="http://www.w3.org/2000/svg"9 xmlns="http://www.w3.org/2000/svg"
8 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"11 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 width="432.071pt"12 width="96"
11 height="445.383pt"13 height="96"
12 viewBox="0 0 432.071 445.383"14 id="svg6517"
13 xml:space="preserve"
14 id="svg2"
15 version="1.1"15 version="1.1"
16 inkscape:version="0.48.4 r9939"16 inkscape:version="0.48.4 r9939"
17 sodipodi:docname="icon.svg"><metadata17 sodipodi:docname="icon.svg">
18 id="metadata34"><rdf:RDF><cc:Work18 <defs
19 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type19 id="defs6519">
20 rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs20 <linearGradient
21 id="defs32" /><sodipodi:namedview21 id="linearGradient3947">
22 <stop
23 style="stop-color:#ffffff;stop-opacity:1;"
24 offset="0"
25 id="stop3949" />
26 <stop
27 style="stop-color:#ffffff;stop-opacity:0;"
28 offset="1"
29 id="stop3951" />
30 </linearGradient>
31 <linearGradient
32 id="Background">
33 <stop
34 id="stop4178"
35 offset="0"
36 style="stop-color:#b8b8b8;stop-opacity:1" />
37 <stop
38 id="stop4180"
39 offset="1"
40 style="stop-color:#c9c9c9;stop-opacity:1" />
41 </linearGradient>
42 <filter
43 style="color-interpolation-filters:sRGB;"
44 inkscape:label="Inner Shadow"
45 id="filter1121">
46 <feFlood
47 flood-opacity="0.59999999999999998"
48 flood-color="rgb(0,0,0)"
49 result="flood"
50 id="feFlood1123" />
51 <feComposite
52 in="flood"
53 in2="SourceGraphic"
54 operator="out"
55 result="composite1"
56 id="feComposite1125" />
57 <feGaussianBlur
58 in="composite1"
59 stdDeviation="1"
60 result="blur"
61 id="feGaussianBlur1127" />
62 <feOffset
63 dx="0"
64 dy="2"
65 result="offset"
66 id="feOffset1129" />
67 <feComposite
68 in="offset"
69 in2="SourceGraphic"
70 operator="atop"
71 result="composite2"
72 id="feComposite1131" />
73 </filter>
74 <filter
75 style="color-interpolation-filters:sRGB;"
76 inkscape:label="Drop Shadow"
77 id="filter950">
78 <feFlood
79 flood-opacity="0.25"
80 flood-color="rgb(0,0,0)"
81 result="flood"
82 id="feFlood952" />
83 <feComposite
84 in="flood"
85 in2="SourceGraphic"
86 operator="in"
87 result="composite1"
88 id="feComposite954" />
89 <feGaussianBlur
90 in="composite1"
91 stdDeviation="1"
92 result="blur"
93 id="feGaussianBlur956" />
94 <feOffset
95 dx="0"
96 dy="1"
97 result="offset"
98 id="feOffset958" />
99 <feComposite
100 in="SourceGraphic"
101 in2="offset"
102 operator="over"
103 result="composite2"
104 id="feComposite960" />
105 </filter>
106 <clipPath
107 clipPathUnits="userSpaceOnUse"
108 id="clipPath873">
109 <g
110 transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
111 id="g875"
112 inkscape:label="Layer 1"
113 style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
114 <path
115 style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
116 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"
117 id="path877"
118 inkscape:connector-curvature="0"
119 sodipodi:nodetypes="sssssssss" />
120 </g>
121 </clipPath>
122 <filter
123 inkscape:collect="always"
124 id="filter891"
125 inkscape:label="Badge Shadow">
126 <feGaussianBlur
127 inkscape:collect="always"
128 stdDeviation="0.71999962"
129 id="feGaussianBlur893" />
130 </filter>
131 </defs>
132 <sodipodi:namedview
133 id="base"
22 pagecolor="#ffffff"134 pagecolor="#ffffff"
23 bordercolor="#666666"135 bordercolor="#666666"
24 borderopacity="1"136 borderopacity="1.0"
25 objecttolerance="10"137 inkscape:pageopacity="0.0"
26 gridtolerance="10"
27 guidetolerance="10"
28 inkscape:pageopacity="0"
29 inkscape:pageshadow="2"138 inkscape:pageshadow="2"
30 inkscape:window-width="1111"139 inkscape:zoom="4.0745362"
31 inkscape:window-height="783"140 inkscape:cx="49.256842"
32 id="namedview30"141 inkscape:cy="43.407288"
33 showgrid="false"142 inkscape:document-units="px"
34 inkscape:zoom="0.42390481"143 inkscape:current-layer="layer3"
35 inkscape:cx="270.04437"144 showgrid="true"
36 inkscape:cy="278.36438"145 fit-margin-top="0"
37 inkscape:window-x="195"146 fit-margin-left="0"
38 inkscape:window-y="148"147 fit-margin-right="0"
148 fit-margin-bottom="0"
149 inkscape:window-width="1417"
150 inkscape:window-height="799"
151 inkscape:window-x="65"
152 inkscape:window-y="24"
39 inkscape:window-maximized="0"153 inkscape:window-maximized="0"
40 inkscape:current-layer="svg2" /><g
41 id="orginal"
42 style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;" /><g
43 id="Layer_x0020_3"
44 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
45 style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;"
46 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"
47 id="path6" /><path
48 style="fill:#336791;stroke:none;"
49 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"
50 id="path8" /><path
51 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"
52 id="path10" /><path
53 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"
54 id="path12" /><path
55 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"
56 id="path14" /><path
57 style="stroke-linejoin:bevel;"
58 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"
59 id="path16" /><path
60 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"
61 id="path18" /><path
62 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"
63 id="path20" /><path
64 style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;"
65 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"
66 id="path22" /><path
67 style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;"
68 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"
69 id="path24" /><path
70 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"
71 id="path26" /><path
72 style="stroke-width:3;"
73 d="M0,60.232"
74 id="path28" /></g><rect
75 style="fill:#4d4d4d;stroke:#000000;stroke-width:0.73861116"
76 id="rect3015"
77 width="258.81003"
78 height="105.94549"
79 x="-2.0179098"
80 y="341.45541" /><text
81 xml:space="preserve"
82 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"
83 x="10.404695"
84 y="396.7569"
85 id="text3011-6"
86 sodipodi:linespacing="125%"
87 transform="scale(0.96172302,1.0398004)"><tspan
88 sodipodi:role="line"
89 id="tspan3013-6"
90 x="10.404695"
91 y="396.7569">psql&gt;_</tspan></text>
92</svg>
93\ No newline at end of file154\ No newline at end of file
155 showborder="true"
156 showguides="true"
157 inkscape:guide-bbox="true"
158 inkscape:showpageshadow="false">
159 <inkscape:grid
160 type="xygrid"
161 id="grid821" />
162 <sodipodi:guide
163 orientation="1,0"
164 position="16,48"
165 id="guide823" />
166 <sodipodi:guide
167 orientation="0,1"
168 position="64,80"
169 id="guide825" />
170 <sodipodi:guide
171 orientation="1,0"
172 position="80,40"
173 id="guide827" />
174 <sodipodi:guide
175 orientation="0,1"
176 position="64,16"
177 id="guide829" />
178 </sodipodi:namedview>
179 <metadata
180 id="metadata6522">
181 <rdf:RDF>
182 <cc:Work
183 rdf:about="">
184 <dc:format>image/svg+xml</dc:format>
185 <dc:type
186 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
187 <dc:title></dc:title>
188 </cc:Work>
189 </rdf:RDF>
190 </metadata>
191 <g
192 inkscape:label="BACKGROUND"
193 inkscape:groupmode="layer"
194 id="layer1"
195 transform="translate(268,-635.29076)"
196 style="display:inline">
197 <path
198 style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121);opacity:1"
199 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"
200 id="path6455"
201 inkscape:connector-curvature="0"
202 sodipodi:nodetypes="sssssssss" />
203 </g>
204 <g
205 inkscape:groupmode="layer"
206 id="layer3"
207 inkscape:label="PLACE YOUR PICTOGRAM HERE"
208 style="display:inline">
209 <g
210 transform="matrix(0.18231452,0,0,0.16578318,13.59438,4.5326341)"
211 id="Layer_x0020_3"
212 style="fill:none;stroke:#ffffff;stroke-width:12.46510029;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;display:inline">
213 <path
214 inkscape:connector-curvature="0"
215 style="fill:#000000;stroke:#000000;stroke-width:37.39530182;stroke-linecap:butt;stroke-linejoin:miter"
216 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"
217 id="path3095" />
218 <path
219 inkscape:connector-curvature="0"
220 style="fill:#336791;stroke:none"
221 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"
222 id="path3097" />
223 <path
224 inkscape:connector-curvature="0"
225 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"
226 id="path3099" />
227 <path
228 inkscape:connector-curvature="0"
229 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"
230 id="path3101" />
231 <path
232 inkscape:connector-curvature="0"
233 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"
234 id="path3103" />
235 <path
236 inkscape:connector-curvature="0"
237 style="stroke-linejoin:bevel"
238 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"
239 id="path3105" />
240 <path
241 inkscape:connector-curvature="0"
242 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"
243 id="path3107" />
244 <path
245 inkscape:connector-curvature="0"
246 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"
247 id="path3109" />
248 <path
249 inkscape:connector-curvature="0"
250 style="fill:#ffffff;stroke-width:4.15500021;stroke-linecap:butt;stroke-linejoin:miter"
251 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"
252 id="path3111" />
253 <path
254 inkscape:connector-curvature="0"
255 style="fill:#ffffff;stroke-width:2.0775001;stroke-linecap:butt;stroke-linejoin:miter"
256 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"
257 id="path3113" />
258 <path
259 inkscape:connector-curvature="0"
260 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"
261 id="path3115" />
262 <path
263 inkscape:connector-curvature="0"
264 style="stroke-width:3"
265 d="M 0,60.232"
266 id="path3117" />
267 </g>
268 <rect
269 style="fill:#4d4d4d;stroke:#000000;stroke-width:0.12356114"
270 id="rect3015"
271 width="40.634895"
272 height="18.884109"
273 x="8.1120977"
274 y="67.328201" />
275 <text
276 xml:space="preserve"
277 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"
278 x="11.078934"
279 y="72.209114"
280 id="text3011-6"
281 sodipodi:linespacing="125%"
282 transform="scale(0.9026139,1.1078934)"><tspan
283 sodipodi:role="line"
284 id="tspan3013-6"
285 x="11.078934"
286 y="72.209114">psql&gt;_</tspan></text>
287 </g>
288 <g
289 inkscape:groupmode="layer"
290 id="layer2"
291 inkscape:label="BADGE"
292 style="display:none"
293 sodipodi:insensitive="true">
294 <g
295 style="display:inline"
296 transform="translate(-340.00001,-581)"
297 id="g4394"
298 clip-path="none">
299 <g
300 id="g855">
301 <g
302 inkscape:groupmode="maskhelper"
303 id="g870"
304 clip-path="url(#clipPath873)"
305 style="opacity:0.6;filter:url(#filter891)">
306 <path
307 transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
308 d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
309 sodipodi:ry="12"
310 sodipodi:rx="12"
311 sodipodi:cy="552.36218"
312 sodipodi:cx="252"
313 id="path844"
314 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"
315 sodipodi:type="arc" />
316 </g>
317 <g
318 id="g862">
319 <path
320 sodipodi:type="arc"
321 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"
322 id="path4398"
323 sodipodi:cx="252"
324 sodipodi:cy="552.36218"
325 sodipodi:rx="12"
326 sodipodi:ry="12"
327 d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
328 transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
329 <path
330 transform="matrix(1.25,0,0,1.25,33,-100.45273)"
331 d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
332 sodipodi:ry="12"
333 sodipodi:rx="12"
334 sodipodi:cy="552.36218"
335 sodipodi:cx="252"
336 id="path4400"
337 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"
338 sodipodi:type="arc" />
339 <path
340 sodipodi:type="star"
341 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"
342 id="path4459"
343 sodipodi:sides="5"
344 sodipodi:cx="666.19574"
345 sodipodi:cy="589.50385"
346 sodipodi:r1="7.2431178"
347 sodipodi:r2="4.3458705"
348 sodipodi:arg1="1.0471976"
349 sodipodi:arg2="1.6755161"
350 inkscape:flatsided="false"
351 inkscape:rounded="0.1"
352 inkscape:randomized="0"
353 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"
354 transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
355 </g>
356 </g>
357 </g>
358 </g>
359</svg>

Subscribers

People subscribed via source and target branches