Merge lp:~stub/charms/precise/postgresql/use-charm-helpers into lp:charms/postgresql
- Precise Pangolin (12.04)
- use-charm-helpers
- Merge into trunk
Proposed by
Stuart Bishop
Status: | Merged |
---|---|
Approved by: | Mark Mims |
Approved revision: | 84 |
Merged at revision: | 55 |
Proposed branch: | lp:~stub/charms/precise/postgresql/use-charm-helpers |
Merge into: | lp:charms/postgresql |
Prerequisite: | lp:~stub/charms/precise/postgresql/replication |
Diff against target: |
1468 lines (+295/-571) 2 files modified
hooks/hooks.py (+283/-569) test.py (+12/-2) |
To merge this branch: | bzr merge lp:~stub/charms/precise/postgresql/use-charm-helpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mark Mims (community) | Approve | ||
Review via email: mp+173470@code.launchpad.net |
Commit message
Description of the change
Use charm-helpers everywhere appropriate, removing our own unnecessary and untested helpers.
To post a comment you must log in.
- 84. By Stuart Bishop
-
Merged replication into use-charm-helpers.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'hooks/hooks.py' |
2 | --- hooks/hooks.py 2013-07-08 11:07:29 +0000 |
3 | +++ hooks/hooks.py 2013-07-08 11:07:29 +0000 |
4 | @@ -5,7 +5,6 @@ |
5 | import cPickle as pickle |
6 | import glob |
7 | from grp import getgrnam |
8 | -import json |
9 | import os.path |
10 | from pwd import getpwnam |
11 | import random |
12 | @@ -15,16 +14,16 @@ |
13 | import string |
14 | import subprocess |
15 | import sys |
16 | -from textwrap import dedent |
17 | import time |
18 | import yaml |
19 | from yaml.constructor import ConstructorError |
20 | |
21 | -from charmhelpers.core import hookenv |
22 | - |
23 | +from charmhelpers.core import hookenv, host |
24 | from charmhelpers.core.hookenv import ( |
25 | - log, CRITICAL, ERROR, WARNING, INFO, DEBUG) |
26 | + CRITICAL, ERROR, WARNING, INFO, DEBUG, log, |
27 | + ) |
28 | |
29 | +hooks = hookenv.Hooks() |
30 | |
31 | # jinja2 may not be importable until the install hook has installed the |
32 | # required packages. |
33 | @@ -33,23 +32,26 @@ |
34 | return Template(*args, **kw) |
35 | |
36 | |
37 | -############################################################################### |
38 | -# Supporting functions |
39 | -############################################################################### |
40 | -MSG_CRITICAL = "CRITICAL" |
41 | -MSG_DEBUG = "DEBUG" |
42 | -MSG_INFO = "INFO" |
43 | -MSG_ERROR = "ERROR" |
44 | -MSG_WARNING = "WARNING" |
45 | - |
46 | - |
47 | -def juju_log(level, msg): |
48 | - log(msg, level) |
49 | +def write_file(path, contents, owner='root', group='root', perms=0o444): |
50 | + '''Temporary alternative to charm-helpers write_file(). |
51 | + |
52 | + charm-helpers' write_file() magic makes it useless for any file |
53 | + containing curly brackets, so work around for now until the feature |
54 | + can be discussed. |
55 | + ''' |
56 | + log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) |
57 | + uid = getpwnam(owner).pw_uid |
58 | + gid = getgrnam(group).gr_gid |
59 | + dest_fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, perms) |
60 | + os.fchown(dest_fd, uid, gid) |
61 | + with os.fdopen(dest_fd, 'w') as destfile: |
62 | + destfile.write(str(contents)) |
63 | |
64 | |
65 | class State(dict): |
66 | """Encapsulate state common to the unit for republishing to relations.""" |
67 | def __init__(self, state_file): |
68 | + super(State, self).__init__() |
69 | self._state_file = state_file |
70 | self.load() |
71 | |
72 | @@ -103,7 +105,6 @@ |
73 | |
74 | |
75 | ############################################################################### |
76 | - |
77 | # Volume managment |
78 | ############################################################################### |
79 | #------------------------------ |
80 | @@ -119,7 +120,7 @@ |
81 | if volume_map: |
82 | return volume_map.get(os.environ['JUJU_UNIT_NAME']) |
83 | except ConstructorError as e: |
84 | - juju_log(MSG_WARNING, "invalid YAML in 'volume-map': %s", e) |
85 | + log("invalid YAML in 'volume-map': {}".format(e), WARNING) |
86 | return None |
87 | |
88 | |
89 | @@ -150,18 +151,21 @@ |
90 | def volume_get_volume_id(): |
91 | ephemeral_storage = config_data['volume-ephemeral-storage'] |
92 | volid = volume_get_volid_from_volume_map() |
93 | - juju_unit_name = os.environ['JUJU_UNIT_NAME'] |
94 | + juju_unit_name = hookenv.local_unit() |
95 | if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']: |
96 | if volid: |
97 | - juju_log(MSG_ERROR, "volume-ephemeral-storage is True, but " + |
98 | - "volume-map['%s'] -> %s" % (juju_unit_name, volid)) |
99 | + log( |
100 | + "volume-ephemeral-storage is True, but " + |
101 | + "volume-map[{!r}] -> {}".format(juju_unit_name, volid), ERROR) |
102 | return None |
103 | else: |
104 | return "--ephemeral" |
105 | else: |
106 | if not volid: |
107 | - juju_log(MSG_ERROR, "volume-ephemeral-storage is False, but " + |
108 | - "no volid found for volume-map['%s']" % (juju_unit_name)) |
109 | + log( |
110 | + "volume-ephemeral-storage is False, but " |
111 | + "no volid found for volume-map[{!r}]".format( |
112 | + hookenv.local_unit()), ERROR) |
113 | return None |
114 | return volid |
115 | |
116 | @@ -191,7 +195,7 @@ |
117 | def enable_service_start(service): |
118 | ### NOTE: doesn't implement per-service, this can be an issue |
119 | ### for colocated charms (subordinates) |
120 | - juju_log(MSG_INFO, "NOTICE: enabling %s start by policy-rc.d" % service) |
121 | + log("enabling {} start by policy-rc.d".format(service)) |
122 | if os.path.exists('/usr/sbin/policy-rc.d'): |
123 | os.unlink('/usr/sbin/policy-rc.d') |
124 | return True |
125 | @@ -199,10 +203,10 @@ |
126 | |
127 | |
128 | def disable_service_start(service): |
129 | - juju_log(MSG_INFO, "NOTICE: disabling %s start by policy-rc.d" % service) |
130 | + log("disabling {} start by policy-rc.d".format(service)) |
131 | policy_rc = '/usr/sbin/policy-rc.d' |
132 | - policy_rc_tmp = "%s.tmp" % policy_rc |
133 | - open('%s' % policy_rc_tmp, 'w').write("""#!/bin/bash |
134 | + policy_rc_tmp = "{}.tmp".format(policy_rc) |
135 | + open(policy_rc_tmp, 'w').write("""#!/bin/bash |
136 | [[ "$1"-"$2" == %s-start ]] && exit 101 |
137 | exit 0 |
138 | EOF |
139 | @@ -211,16 +215,14 @@ |
140 | os.rename(policy_rc_tmp, policy_rc) |
141 | |
142 | |
143 | -#------------------------------------------------------------------------------ |
144 | -# run: Run a command, return the output |
145 | -#------------------------------------------------------------------------------ |
146 | def run(command, exit_on_error=True): |
147 | + '''Run a command and return the output.''' |
148 | try: |
149 | - juju_log(MSG_DEBUG, command) |
150 | + log(command, DEBUG) |
151 | return subprocess.check_output( |
152 | command, stderr=subprocess.STDOUT, shell=True) |
153 | except subprocess.CalledProcessError, e: |
154 | - juju_log(MSG_ERROR, "status=%d, output=%s" % (e.returncode, e.output)) |
155 | + log("status=%d, output=%s" % (e.returncode, e.output), ERROR) |
156 | if exit_on_error: |
157 | sys.exit(e.returncode) |
158 | else: |
159 | @@ -228,27 +230,6 @@ |
160 | |
161 | |
162 | #------------------------------------------------------------------------------ |
163 | -# install_file: install a file resource. overwites existing files. |
164 | -#------------------------------------------------------------------------------ |
165 | -def install_file(contents, dest, owner="root", group="root", mode=0600): |
166 | - uid = getpwnam(owner)[2] |
167 | - gid = getgrnam(group)[2] |
168 | - dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode) |
169 | - os.fchown(dest_fd, uid, gid) |
170 | - with os.fdopen(dest_fd, 'w') as destfile: |
171 | - destfile.write(str(contents)) |
172 | - |
173 | - |
174 | -#------------------------------------------------------------------------------ |
175 | -# install_dir: create a directory |
176 | -#------------------------------------------------------------------------------ |
177 | -def install_dir(dirname, owner="root", group="root", mode=0700): |
178 | - command = '/usr/bin/install -o {} -g {} -m {} -d {}'.format( |
179 | - owner, group, oct(mode), dirname) |
180 | - return run(command) |
181 | - |
182 | - |
183 | -#------------------------------------------------------------------------------ |
184 | # postgresql_stop, postgresql_start, postgresql_is_running: |
185 | # wrappers over invoke-rc.d, with extra check for postgresql_is_running() |
186 | #------------------------------------------------------------------------------ |
187 | @@ -272,7 +253,7 @@ |
188 | def postgresql_start(): |
189 | status, output = commands.getstatusoutput("invoke-rc.d postgresql start") |
190 | if status != 0: |
191 | - juju_log(MSG_CRITICAL, output) |
192 | + log(output, CRITICAL) |
193 | return False |
194 | return postgresql_is_running() |
195 | |
196 | @@ -287,11 +268,8 @@ |
197 | last_warning = time.time() |
198 | while postgresql_is_in_backup_mode(): |
199 | if time.time() + 120 > last_warning: |
200 | - juju_log( |
201 | - MSG_WARNING, |
202 | - "In backup mode. PostgreSQL restart blocked.") |
203 | - juju_log( |
204 | - MSG_INFO, |
205 | + log("In backup mode. PostgreSQL restart blocked.", WARNING) |
206 | + log( |
207 | "Run \"psql -U postgres -c 'SELECT pg_stop_backup()'\"" |
208 | "to cancel backup mode and forcefully unblock this hook.") |
209 | last_warning = time.time() |
210 | @@ -346,9 +324,8 @@ |
211 | |
212 | if new_value != live_value: |
213 | if live_config: |
214 | - juju_log( |
215 | - MSG_DEBUG, "Changed {} from {} to {}".format( |
216 | - name, repr(live_value), repr(new_value))) |
217 | + log("Changed {} from {!r} to {!r}".format( |
218 | + name, live_value, new_value), DEBUG) |
219 | if context == 'postmaster': |
220 | # A setting has changed that requires PostgreSQL to be |
221 | # restarted before it will take effect. |
222 | @@ -356,14 +333,12 @@ |
223 | |
224 | if requires_restart: |
225 | # A change has been requested that requires a restart. |
226 | - juju_log( |
227 | - MSG_WARNING, |
228 | - "Configuration change requires PostgreSQL restart. " |
229 | - "Restarting.") |
230 | + log( |
231 | + "Configuration change requires PostgreSQL restart. Restarting.", |
232 | + WARNING) |
233 | rc = postgresql_restart() |
234 | else: |
235 | - juju_log( |
236 | - MSG_DEBUG, "PostgreSQL reload, config changes taking effect.") |
237 | + log("PostgreSQL reload, config changes taking effect.", DEBUG) |
238 | rc = postgresql_reload() # No pending need to bounce, just reload. |
239 | |
240 | if rc == 0 and 'saved_config' in local_state: |
241 | @@ -373,178 +348,20 @@ |
242 | return rc |
243 | |
244 | |
245 | -#------------------------------------------------------------------------------ |
246 | -# config_get: Returns a dictionary containing all of the config information |
247 | -# Optional parameter: scope |
248 | -# scope: limits the scope of the returned configuration to the |
249 | -# desired config item. |
250 | -#------------------------------------------------------------------------------ |
251 | -def config_get(scope=None): |
252 | - try: |
253 | - config_cmd_line = ['config-get'] |
254 | - if scope is not None: |
255 | - config_cmd_line.append(scope) |
256 | - config_cmd_line.append('--format=json') |
257 | - config_data = json.loads(subprocess.check_output(config_cmd_line)) |
258 | - except: |
259 | - config_data = None |
260 | - finally: |
261 | - return(config_data) |
262 | - |
263 | - |
264 | -#------------------------------------------------------------------------------ |
265 | -# get_service_port: Convenience function that scans the existing postgresql |
266 | -# configuration file and returns a the existing port |
267 | -# being used. This is necessary to know which port(s) |
268 | -# to open and close when exposing/unexposing a service |
269 | -#------------------------------------------------------------------------------ |
270 | def get_service_port(postgresql_config): |
271 | - postgresql_config = load_postgresql_config(postgresql_config) |
272 | - if postgresql_config is None: |
273 | - return(None) |
274 | + '''Return the port PostgreSQL is listening on.''' |
275 | + if not os.path.exists(postgresql_config): |
276 | + return None |
277 | + postgresql_config = open(postgresql_config, 'r').read() |
278 | port = re.search("port.*=(.*)", postgresql_config).group(1).strip() |
279 | try: |
280 | return int(port) |
281 | - except: |
282 | - return None |
283 | - |
284 | - |
285 | -#------------------------------------------------------------------------------ |
286 | -# relation_json: Returns json-formatted relation data |
287 | -# Optional parameters: scope, relation_id |
288 | -# scope: limits the scope of the returned data to the |
289 | -# desired item. |
290 | -# unit_name: limits the data ( and optionally the scope ) |
291 | -# to the specified unit |
292 | -# relation_id: specify relation id for out of context usage. |
293 | -#------------------------------------------------------------------------------ |
294 | -def relation_json(scope=None, unit_name=None, relation_id=None): |
295 | - command = ['relation-get', '--format=json'] |
296 | - if relation_id is not None: |
297 | - command.extend(('-r', relation_id)) |
298 | - if scope is not None: |
299 | - command.append(scope) |
300 | - else: |
301 | - command.append('-') |
302 | - if unit_name is not None: |
303 | - command.append(unit_name) |
304 | - output = subprocess.check_output(command, stderr=subprocess.STDOUT) |
305 | - return output or None |
306 | - |
307 | - |
308 | -#------------------------------------------------------------------------------ |
309 | -# relation_get: Returns a dictionary containing the relation information |
310 | -# Optional parameters: scope, relation_id |
311 | -# scope: limits the scope of the returned data to the |
312 | -# desired item. |
313 | -# unit_name: limits the data ( and optionally the scope ) |
314 | -# to the specified unit |
315 | -#------------------------------------------------------------------------------ |
316 | -def relation_get(scope=None, unit_name=None, relation_id=None): |
317 | - j = relation_json(scope, unit_name, relation_id) |
318 | - if j: |
319 | - return json.loads(j) |
320 | - else: |
321 | - return None |
322 | - |
323 | - |
324 | -def relation_set(keyvalues, relation_id=None): |
325 | - args = [] |
326 | - if relation_id: |
327 | - args.extend(['-r', relation_id]) |
328 | - args.extend(["{}='{}'".format(k, v or '') for k, v in keyvalues.items()]) |
329 | - run("relation-set {}".format(' '.join(args))) |
330 | - |
331 | - ## Posting json to relation-set doesn't seem to work as documented? |
332 | - ## Bug #1116179 |
333 | - ## |
334 | - ## cmd = ['relation-set'] |
335 | - ## if relation_id: |
336 | - ## cmd.extend(['-r', relation_id]) |
337 | - ## p = Popen( |
338 | - ## cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, |
339 | - ## stderr=subprocess.PIPE) |
340 | - ## (out, err) = p.communicate(json.dumps(keyvalues)) |
341 | - ## if p.returncode: |
342 | - ## juju_log(MSG_ERROR, err) |
343 | - ## sys.exit(1) |
344 | - ## juju_log(MSG_DEBUG, "relation-set {}".format(repr(keyvalues))) |
345 | - |
346 | - |
347 | -def relation_list(relation_id=None): |
348 | - """Return the list of units participating in the relation.""" |
349 | - if relation_id is None: |
350 | - relation_id = os.environ['JUJU_RELATION_ID'] |
351 | - cmd = ['relation-list', '--format=json', '-r', relation_id] |
352 | - json_units = subprocess.check_output(cmd).strip() |
353 | - if json_units: |
354 | - data = json.loads(json_units) |
355 | - if data is not None: |
356 | - return data |
357 | - return [] |
358 | - |
359 | - |
360 | -#------------------------------------------------------------------------------ |
361 | -# relation_ids: Returns a list of relation ids |
362 | -# optional parameters: relation_type |
363 | -# relation_type: return relations only of this type |
364 | -#------------------------------------------------------------------------------ |
365 | -def relation_ids(relation_types=('db',)): |
366 | - # accept strings or iterators |
367 | - if isinstance(relation_types, basestring): |
368 | - reltypes = [relation_types, ] |
369 | - else: |
370 | - reltypes = relation_types |
371 | - relids = [] |
372 | - for reltype in reltypes: |
373 | - relid_cmd_line = ['relation-ids', '--format=json', reltype] |
374 | - json_relids = subprocess.check_output(relid_cmd_line).strip() |
375 | - if json_relids: |
376 | - relids.extend(json.loads(json_relids)) |
377 | - return relids |
378 | - |
379 | - |
380 | -#------------------------------------------------------------------------------ |
381 | -# relation_get_all: Returns a dictionary containing the relation information |
382 | -# optional parameters: relation_type |
383 | -# relation_type: limits the scope of the returned data to the |
384 | -# desired item. |
385 | -#------------------------------------------------------------------------------ |
386 | -def relation_get_all(*args, **kwargs): |
387 | - relation_data = [] |
388 | - relids = relation_ids(*args, **kwargs) |
389 | - for relid in relids: |
390 | - units_cmd_line = ['relation-list', '--format=json', '-r', relid] |
391 | - json_units = subprocess.check_output(units_cmd_line).strip() |
392 | - if json_units: |
393 | - for unit in json.loads(json_units): |
394 | - unit_data = \ |
395 | - json.loads(relation_json(relation_id=relid, |
396 | - unit_name=unit)) |
397 | - for key in unit_data: |
398 | - if key.endswith('-list'): |
399 | - unit_data[key] = unit_data[key].split() |
400 | - unit_data['relation-id'] = relid |
401 | - unit_data['unit'] = unit |
402 | - relation_data.append(unit_data) |
403 | - return relation_data |
404 | - |
405 | - |
406 | -#------------------------------------------------------------------------------ |
407 | -# apt_get_install( packages ): Installs package(s) |
408 | -#------------------------------------------------------------------------------ |
409 | -def apt_get_install(packages=None): |
410 | - if packages is None: |
411 | - return(False) |
412 | - cmd_line = ['apt-get', '-y', 'install', '-qq'] |
413 | - cmd_line.extend(packages) |
414 | - return(subprocess.call(cmd_line)) |
415 | - |
416 | - |
417 | -#------------------------------------------------------------------------------ |
418 | -# create_postgresql_config: Creates the postgresql.conf file |
419 | -#------------------------------------------------------------------------------ |
420 | + except (ValueError, TypeError): |
421 | + return None |
422 | + |
423 | + |
424 | def create_postgresql_config(postgresql_config): |
425 | + '''Create the postgresql.conf file''' |
426 | if config_data["performance_tuning"] == "auto": |
427 | # Taken from: |
428 | # http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server |
429 | @@ -575,9 +392,8 @@ |
430 | # certain minimum levels. |
431 | num_slaves = slave_count() |
432 | if num_slaves > 0: |
433 | - juju_log( |
434 | - MSG_INFO, '{} hot standbys in peer relation.'.format(num_slaves)) |
435 | - juju_log(MSG_INFO, 'Ensuring minimal replication settings') |
436 | + log('{} hot standbys in peer relation.'.format(num_slaves)) |
437 | + log('Ensuring minimal replication settings') |
438 | config_data['hot_standby'] = 'on' |
439 | config_data['wal_level'] = 'hot_standby' |
440 | config_data['max_wal_senders'] = max( |
441 | @@ -590,29 +406,27 @@ |
442 | # Return it as pg_config |
443 | pg_config = Template( |
444 | open("templates/postgresql.conf.tmpl").read()).render(config_data) |
445 | - install_file(pg_config, postgresql_config) |
446 | + write_file( |
447 | + postgresql_config, pg_config, |
448 | + owner="postgres", group="postgres", perms=0600) |
449 | |
450 | local_state['saved_config'] = config_data |
451 | local_state.save() |
452 | |
453 | |
454 | -#------------------------------------------------------------------------------ |
455 | -# create_postgresql_ident: Creates the pg_ident.conf file |
456 | -#------------------------------------------------------------------------------ |
457 | def create_postgresql_ident(postgresql_ident): |
458 | + '''Create the pg_ident.conf file.''' |
459 | ident_data = {} |
460 | - pg_ident_template = \ |
461 | - Template( |
462 | - open("templates/pg_ident.conf.tmpl").read()).render(ident_data) |
463 | - with open(postgresql_ident, 'w') as ident_file: |
464 | - ident_file.write(str(pg_ident_template)) |
465 | - |
466 | - |
467 | -#------------------------------------------------------------------------------ |
468 | -# generate_postgresql_hba: Creates the pg_hba.conf file |
469 | -#------------------------------------------------------------------------------ |
470 | -def generate_postgresql_hba(postgresql_hba, user=None, |
471 | - schema_user=None, database=None): |
472 | + pg_ident_template = Template( |
473 | + open("templates/pg_ident.conf.tmpl").read()) |
474 | + write_file( |
475 | + postgresql_ident, pg_ident_template.render(ident_data), |
476 | + owner="postgres", group="postgres", perms=0600) |
477 | + |
478 | + |
479 | +def generate_postgresql_hba( |
480 | + postgresql_hba, user=None, schema_user=None, database=None): |
481 | + '''Create the pg_hba.conf file.''' |
482 | |
483 | # Per Bug #1117542, when generating the postgresql_hba file we |
484 | # need to cope with private-address being either an IP address |
485 | @@ -627,9 +441,10 @@ |
486 | return addr |
487 | |
488 | relation_data = [] |
489 | - for relid in relation_ids(relation_types=['db', 'db-admin']): |
490 | - local_relation = relation_get( |
491 | - unit_name=os.environ['JUJU_UNIT_NAME'], relation_id=relid) |
492 | + relids = hookenv.relation_ids('db') + hookenv.relation_ids('db-admin') |
493 | + for relid in relids: |
494 | + local_relation = hookenv.relation_get( |
495 | + unit=hookenv.local_unit(), rid=relid) |
496 | |
497 | # We might see relations that have not yet been setup enough. |
498 | # At a minimum, the relation-joined hook needs to have been run |
499 | @@ -638,8 +453,8 @@ |
500 | if 'user' not in local_relation: |
501 | continue |
502 | |
503 | - for unit in relation_list(relid): |
504 | - relation = relation_get(unit_name=unit, relation_id=relid) |
505 | + for unit in hookenv.related_units(relid): |
506 | + relation = hookenv.relation_get(unit=unit, rid=relid) |
507 | |
508 | relation['relation-id'] = relid |
509 | relation['unit'] = unit |
510 | @@ -666,15 +481,15 @@ |
511 | relation['private-address']) |
512 | relation_data.append(relation) |
513 | |
514 | - juju_log(MSG_INFO, str(relation_data)) |
515 | + log(str(relation_data), INFO) |
516 | |
517 | # Replication connections. Each unit needs to be able to connect to |
518 | # every other unit's postgres database and the magic replication |
519 | # database. It also needs to be able to connect to its own postgres |
520 | # database. |
521 | - for relid in relation_ids(relation_types=replication_relation_types): |
522 | - for unit in relation_list(relid): |
523 | - relation = relation_get(unit_name=unit, relation_id=relid) |
524 | + for relid in hookenv.relation_ids('replication'): |
525 | + for unit in hookenv.related_units(relid): |
526 | + relation = hookenv.relation_get(unit=unit, rid=relid) |
527 | remote_addr = munge_address(relation['private-address']) |
528 | remote_replication = {'database': 'replication', |
529 | 'user': 'juju_replication', |
530 | @@ -692,27 +507,25 @@ |
531 | relation_data.append(remote_pgdb) |
532 | |
533 | # Hooks need permissions too to setup replication. |
534 | - for relid in relation_ids(relation_types=['replication']): |
535 | + for relid in hookenv.relation_ids('replication'): |
536 | local_replication = {'database': 'postgres', |
537 | 'user': 'juju_replication', |
538 | - 'private-address': munge_address(get_unit_host()), |
539 | + 'private-address': munge_address( |
540 | + hookenv.unit_private_ip()), |
541 | 'relation-id': relid, |
542 | - 'unit': os.environ['JUJU_UNIT_NAME'], |
543 | + 'unit': hookenv.local_unit(), |
544 | } |
545 | relation_data.append(local_replication) |
546 | |
547 | - pg_hba_template = Template( |
548 | - open("templates/pg_hba.conf.tmpl").read()).render( |
549 | - access_list=relation_data) |
550 | - with open(postgresql_hba, 'w') as hba_file: |
551 | - hba_file.write(str(pg_hba_template)) |
552 | + pg_hba_template = Template(open("templates/pg_hba.conf.tmpl").read()) |
553 | + write_file( |
554 | + postgresql_hba, pg_hba_template.render(access_list=relation_data), |
555 | + owner="postgres", group="postgres", perms=0600) |
556 | postgresql_reload() |
557 | |
558 | |
559 | -#------------------------------------------------------------------------------ |
560 | -# install_postgresql_crontab: Creates the postgresql crontab file |
561 | -#------------------------------------------------------------------------------ |
562 | def install_postgresql_crontab(postgresql_ident): |
563 | + '''Create the postgres user's crontab''' |
564 | crontab_data = { |
565 | 'backup_schedule': config_data["backup_schedule"], |
566 | 'scripts_dir': postgresql_scripts_dir, |
567 | @@ -720,7 +533,28 @@ |
568 | } |
569 | crontab_template = Template( |
570 | open("templates/postgres.cron.tmpl").read()).render(crontab_data) |
571 | - install_file(str(crontab_template), "/etc/cron.d/postgres", mode=0644) |
572 | + write_file('/etc/cron.d/postgres', crontab_template, perms=0600) |
573 | + |
574 | + |
575 | +def create_recovery_conf(master_host, password, restart_on_change=False): |
576 | + recovery_conf_path = os.path.join(postgresql_cluster_dir, 'recovery.conf') |
577 | + if os.path.exists(recovery_conf_path): |
578 | + old_recovery_conf = open(recovery_conf_path, 'r').read() |
579 | + else: |
580 | + old_recovery_conf = None |
581 | + |
582 | + recovery_conf = Template( |
583 | + open("templates/recovery.conf.tmpl").read()).render({ |
584 | + 'host': master_host, |
585 | + 'password': local_state['replication_password']}) |
586 | + log(recovery_conf, DEBUG) |
587 | + write_file( |
588 | + os.path.join(postgresql_cluster_dir, 'recovery.conf'), |
589 | + recovery_conf, owner="postgres", group="postgres", perms=0o600) |
590 | + |
591 | + if restart_on_change and old_recovery_conf != recovery_conf: |
592 | + log("recovery.conf updated. Restarting to take effect.") |
593 | + postgresql_restart() |
594 | |
595 | |
596 | #------------------------------------------------------------------------------ |
597 | @@ -737,28 +571,6 @@ |
598 | |
599 | |
600 | #------------------------------------------------------------------------------ |
601 | -# open_port: Convenience function to open a port in juju to |
602 | -# expose a service |
603 | -#------------------------------------------------------------------------------ |
604 | -def open_port(port=None, protocol="TCP"): |
605 | - if port is None: |
606 | - return(None) |
607 | - return(subprocess.call(['open-port', "%d/%s" % |
608 | - (int(port), protocol)])) |
609 | - |
610 | - |
611 | -#------------------------------------------------------------------------------ |
612 | -# close_port: Convenience function to close a port in juju to |
613 | -# unexpose a service |
614 | -#------------------------------------------------------------------------------ |
615 | -def close_port(port=None, protocol="TCP"): |
616 | - if port is None: |
617 | - return(None) |
618 | - return(subprocess.call(['close-port', "%d/%s" % |
619 | - (int(port), protocol)])) |
620 | - |
621 | - |
622 | -#------------------------------------------------------------------------------ |
623 | # update_service_ports: Convenience function that evaluate the old and new |
624 | # service ports to decide which ports need to be |
625 | # opened and which to close |
626 | @@ -767,18 +579,14 @@ |
627 | if old_service_port is None or new_service_port is None: |
628 | return(None) |
629 | if new_service_port != old_service_port: |
630 | - close_port(old_service_port) |
631 | - open_port(new_service_port) |
632 | - |
633 | - |
634 | -#------------------------------------------------------------------------------ |
635 | -# pwgen: Generates a random password |
636 | -# pwd_length: Defines the length of the password to generate |
637 | -# default: 20 |
638 | -#------------------------------------------------------------------------------ |
639 | + hookenv.close_port(old_service_port) |
640 | + hookenv.open_port(new_service_port) |
641 | + |
642 | + |
643 | def pwgen(pwd_length=None): |
644 | + '''Generate a random password.''' |
645 | if pwd_length is None: |
646 | - pwd_length = random.choice(range(20, 30)) |
647 | + pwd_length = random.choice(range(30, 40)) |
648 | alphanumeric_chars = [l for l in (string.letters + string.digits) |
649 | if l not in 'Iil0oO1'] |
650 | random_chars = [random.choice(alphanumeric_chars) |
651 | @@ -825,9 +633,8 @@ |
652 | break |
653 | except psycopg2.Error, x: |
654 | if time.time() > start + timeout: |
655 | - juju_log( |
656 | - MSG_CRITICAL, "Database connection {!r} failed".format( |
657 | - conn_str)) |
658 | + log("Database connection {!r} failed".format( |
659 | + conn_str), CRITICAL) |
660 | raise |
661 | log("Unable to open connection ({}), retrying.".format(x)) |
662 | time.sleep(1) |
663 | @@ -842,7 +649,7 @@ |
664 | cur.execute(sql, parameters) |
665 | return cur.statusmessage |
666 | except psycopg2.ProgrammingError: |
667 | - juju_log(MSG_CRITICAL, sql) |
668 | + log(sql, CRITICAL) |
669 | raise |
670 | |
671 | |
672 | @@ -871,15 +678,16 @@ |
673 | if volid: |
674 | if volume_is_permanent(volid): |
675 | if not volume_init_and_mount(volid): |
676 | - juju_log(MSG_ERROR, |
677 | - "volume_init_and_mount failed, " |
678 | - "not applying changes") |
679 | + log( |
680 | + "volume_init_and_mount failed, not applying changes", |
681 | + ERROR) |
682 | return False |
683 | |
684 | if not os.path.exists(data_directory_path): |
685 | - juju_log(MSG_CRITICAL, |
686 | - "postgresql data dir = %s not found, " |
687 | - "not applying changes." % data_directory_path) |
688 | + log( |
689 | + "postgresql data dir {} not found, " |
690 | + "not applying changes.".format(data_directory_path), |
691 | + CRITICAL) |
692 | return False |
693 | |
694 | mount_point = volume_mount_point_from_volid(volid) |
695 | @@ -887,22 +695,22 @@ |
696 | new_pg_version_cluster_dir = os.path.join( |
697 | new_pg_dir, config_data["version"], config_data["cluster_name"]) |
698 | if not mount_point: |
699 | - juju_log(MSG_ERROR, |
700 | - "invalid mount point from volid = \"%s\", " |
701 | - "not applying changes." % mount_point) |
702 | + log( |
703 | + "invalid mount point from volid = {}, " |
704 | + "not applying changes.".format(mount_point), ERROR) |
705 | return False |
706 | |
707 | if ((os.path.islink(data_directory_path) and |
708 | os.readlink(data_directory_path) == new_pg_version_cluster_dir and |
709 | os.path.isdir(new_pg_version_cluster_dir))): |
710 | - juju_log(MSG_INFO, |
711 | - "NOTICE: postgresql data dir '%s' already points " |
712 | - "to '%s', skipping storage changes." % |
713 | - (data_directory_path, new_pg_version_cluster_dir)) |
714 | - juju_log(MSG_INFO, |
715 | - "existing-symlink: to fix/avoid UID changes from " |
716 | - "previous units, doing: " |
717 | - "chown -R postgres:postgres %s" % new_pg_dir) |
718 | + log( |
719 | + "postgresql data dir '%s' already points " |
720 | + "to {}, skipping storage changes.".format( |
721 | + data_directory_path, new_pg_version_cluster_dir)) |
722 | + log( |
723 | + "existing-symlink: to fix/avoid UID changes from " |
724 | + "previous units, doing: " |
725 | + "chown -R postgres:postgres {}".format(new_pg_dir)) |
726 | run("chown -R postgres:postgres %s" % new_pg_dir) |
727 | return True |
728 | |
729 | @@ -914,7 +722,7 @@ |
730 | os.path.join(new_pg_dir, config_data["version"]), |
731 | new_pg_version_cluster_dir]: |
732 | if not os.path.isdir(new_dir): |
733 | - juju_log(MSG_INFO, "mkdir %s" % new_dir) |
734 | + log("mkdir %s".format(new_dir)) |
735 | os.mkdir(new_dir) |
736 | # copy permissions from current data_directory_path |
737 | os.chown(new_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid) |
738 | @@ -925,45 +733,49 @@ |
739 | # but keep previous "main/" directory, by renaming it to |
740 | # main-$TIMESTAMP |
741 | if not postgresql_stop(): |
742 | - juju_log(MSG_ERROR, |
743 | - "postgresql_stop() returned False - can't migrate data.") |
744 | + log("postgresql_stop() failed - can't migrate data.", ERROR) |
745 | return False |
746 | - if not os.path.exists(os.path.join(new_pg_version_cluster_dir, |
747 | - "PG_VERSION")): |
748 | - juju_log(MSG_WARNING, "migrating PG data %s/ -> %s/" % ( |
749 | - data_directory_path, new_pg_version_cluster_dir)) |
750 | + if not os.path.exists(os.path.join( |
751 | + new_pg_version_cluster_dir, "PG_VERSION")): |
752 | + log("migrating PG data {}/ -> {}/".format( |
753 | + data_directory_path, new_pg_version_cluster_dir), WARNING) |
754 | # void copying PID file to perm storage (shouldn't be any...) |
755 | - command = "rsync -a --exclude postmaster.pid %s/ %s/" % \ |
756 | - (data_directory_path, new_pg_version_cluster_dir) |
757 | - juju_log(MSG_INFO, "run: %s" % command) |
758 | - #output = run(command) |
759 | + command = "rsync -a --exclude postmaster.pid {}/ {}/".format( |
760 | + data_directory_path, new_pg_version_cluster_dir) |
761 | + log("run: {}".format(command)) |
762 | run(command) |
763 | try: |
764 | - os.rename(data_directory_path, "%s-%d" % ( |
765 | + os.rename(data_directory_path, "{}-{}".format( |
766 | data_directory_path, int(time.time()))) |
767 | - juju_log(MSG_INFO, "NOTICE: symlinking %s -> %s" % |
768 | - (new_pg_version_cluster_dir, data_directory_path)) |
769 | + log("NOTICE: symlinking {} -> {}".format( |
770 | + new_pg_version_cluster_dir, data_directory_path)) |
771 | os.symlink(new_pg_version_cluster_dir, data_directory_path) |
772 | - juju_log(MSG_INFO, |
773 | - "after-symlink: to fix/avoid UID changes from " |
774 | - "previous units, doing: " |
775 | - "chown -R postgres:postgres %s" % new_pg_dir) |
776 | - run("chown -R postgres:postgres %s" % new_pg_dir) |
777 | + log( |
778 | + "after-symlink: to fix/avoid UID changes from " |
779 | + "previous units, doing: " |
780 | + "chown -R postgres:postgres {}".format(new_pg_dir)) |
781 | + run("chown -R postgres:postgres {}".format(new_pg_dir)) |
782 | return True |
783 | except OSError: |
784 | - juju_log(MSG_CRITICAL, "failed to symlink \"%s\" -> \"%s\"" % ( |
785 | - data_directory_path, mount_point)) |
786 | + log("failed to symlink {} -> {}".format( |
787 | + data_directory_path, mount_point), CRITICAL) |
788 | return False |
789 | else: |
790 | - juju_log(MSG_ERROR, "ERROR: Invalid volume storage configuration, " + |
791 | - "not applying changes") |
792 | + log( |
793 | + "Invalid volume storage configuration, not applying changes", |
794 | + ERROR) |
795 | return False |
796 | |
797 | |
798 | -############################################################################### |
799 | -# Hook functions |
800 | -############################################################################### |
801 | -def config_changed(postgresql_config, force_restart=False): |
802 | +def token_sql_safe(value): |
803 | + # Only allow alphanumeric + underscore in database identifiers |
804 | + if re.search('[^A-Za-z0-9_]', value): |
805 | + return False |
806 | + return True |
807 | + |
808 | + |
809 | +@hooks.hook() |
810 | +def config_changed(force_restart=False): |
811 | |
812 | add_extra_repos() |
813 | |
814 | @@ -975,11 +787,11 @@ |
815 | postgresql_stop() |
816 | mounts = volume_get_all_mounted() |
817 | if mounts: |
818 | - juju_log(MSG_INFO, "FYI current mounted volumes: %s" % mounts) |
819 | - juju_log(MSG_ERROR, |
820 | - "Disabled and stopped postgresql service, " |
821 | - "because of broken volume configuration - check " |
822 | - "'volume-ephemeral-storage' and 'volume-map'") |
823 | + log("current mounted volumes: {}".format(mounts)) |
824 | + log( |
825 | + "Disabled and stopped postgresql service, " |
826 | + "because of broken volume configuration - check " |
827 | + "'volume-ephemeral-storage' and 'volume-map'", ERROR) |
828 | sys.exit(1) |
829 | |
830 | if volume_is_permanent(volid): |
831 | @@ -992,10 +804,10 @@ |
832 | postgresql_stop() |
833 | mounts = volume_get_all_mounted() |
834 | if mounts: |
835 | - juju_log(MSG_INFO, "FYI current mounted volumes: %s" % mounts) |
836 | - juju_log(MSG_ERROR, |
837 | - "Disabled and stopped postgresql service " |
838 | - "(config_changed_volume_apply failure)") |
839 | + log("current mounted volumes: {}".format(mounts)) |
840 | + log( |
841 | + "Disabled and stopped postgresql service " |
842 | + "(config_changed_volume_apply failure)", ERROR) |
843 | sys.exit(1) |
844 | current_service_port = get_service_port(postgresql_config) |
845 | create_postgresql_config(postgresql_config) |
846 | @@ -1010,13 +822,7 @@ |
847 | return postgresql_reload_or_restart() |
848 | |
849 | |
850 | -def token_sql_safe(value): |
851 | - # Only allow alphanumeric + underscore in database identifiers |
852 | - if re.search('[^A-Za-z0-9_]', value): |
853 | - return False |
854 | - return True |
855 | - |
856 | - |
857 | +@hooks.hook() |
858 | def install(run_pre=True): |
859 | if run_pre: |
860 | for f in glob.glob('exec.d/*/charm-pre-install'): |
861 | @@ -1028,8 +834,9 @@ |
862 | packages = ["postgresql", "pwgen", "python-jinja2", "syslinux", |
863 | "python-psycopg2", "postgresql-contrib", "postgresql-plpython", |
864 | "postgresql-%s-debversion" % config_data["version"]] |
865 | - packages.extend(config_data["extra-packages"].split()) |
866 | - apt_get_install(packages) |
867 | + packages.extend((hookenv.config('extra-packages') or '').split()) |
868 | + packages = host.filter_installed_packages(packages) |
869 | + host.apt_install(packages, fatal=True) |
870 | |
871 | if not 'state' in local_state: |
872 | # Fresh installation. Because this function is invoked by both |
873 | @@ -1045,9 +852,9 @@ |
874 | run("pg_createcluster --locale='{}' --encoding='{}' 9.1 main".format( |
875 | config_data['locale'], config_data['encoding'])) |
876 | |
877 | - install_dir(postgresql_backups_dir, owner="postgres", mode=0755) |
878 | - install_dir(postgresql_scripts_dir, owner="postgres", mode=0755) |
879 | - install_dir(postgresql_logs_dir, owner="postgres", mode=0755) |
880 | + host.mkdir(postgresql_backups_dir, owner="postgres", perms=0o755) |
881 | + host.mkdir(postgresql_scripts_dir, owner="postgres", perms=0o755) |
882 | + host.mkdir(postgresql_logs_dir, owner="postgres", perms=0o755) |
883 | paths = { |
884 | 'base_dir': postgresql_data_dir, |
885 | 'backup_dir': postgresql_backups_dir, |
886 | @@ -1058,25 +865,41 @@ |
887 | open("templates/dump-pg-db.tmpl").read()).render(paths) |
888 | backup_job = Template( |
889 | open("templates/pg_backup_job.tmpl").read()).render(paths) |
890 | - install_file(dump_script, '{}/dump-pg-db'.format(postgresql_scripts_dir), |
891 | - mode=0755) |
892 | - install_file(backup_job, '{}/pg_backup_job'.format(postgresql_scripts_dir), |
893 | - mode=0755) |
894 | + write_file( |
895 | + '{}/dump-pg-db'.format(postgresql_scripts_dir), |
896 | + dump_script, perms=0755) |
897 | + write_file( |
898 | + '{}/pg_backup_job'.format(postgresql_scripts_dir), |
899 | + backup_job, perms=0755) |
900 | install_postgresql_crontab(postgresql_crontab) |
901 | - open_port(5432) |
902 | + hookenv.open_port(5432) |
903 | |
904 | # Ensure at least minimal access granted for hooks to run. |
905 | # Reload because we are using the default cluster setup and started |
906 | # when we installed the PostgreSQL packages. |
907 | - config_changed(postgresql_config, force_restart=True) |
908 | + config_changed(force_restart=True) |
909 | |
910 | snapshot_relations() |
911 | |
912 | |
913 | +@hooks.hook() |
914 | def upgrade_charm(): |
915 | + install(run_pre=False) |
916 | snapshot_relations() |
917 | |
918 | |
919 | +@hooks.hook() |
920 | +def start(): |
921 | + if not postgresql_restart(): |
922 | + raise SystemExit(1) |
923 | + |
924 | + |
925 | +@hooks.hook() |
926 | +def stop(): |
927 | + if not postgresql_stop(): |
928 | + raise SystemExit(1) |
929 | + |
930 | + |
931 | def quote_identifier(identifier): |
932 | r'''Quote an identifier, such as a table or role name. |
933 | |
934 | @@ -1220,20 +1043,6 @@ |
935 | AsIs(quote_identifier(user))) |
936 | |
937 | |
938 | -def get_relation_host(): |
939 | - remote_host = run("relation-get ip") |
940 | - if not remote_host: |
941 | - # remote unit $JUJU_REMOTE_UNIT uses deprecated 'ip=' component of |
942 | - # interface. |
943 | - remote_host = run("relation-get private-address") |
944 | - return remote_host |
945 | - |
946 | - |
947 | -def get_unit_host(): |
948 | - this_host = run("unit-get private-address") |
949 | - return this_host.strip() |
950 | - |
951 | - |
952 | def snapshot_relations(): |
953 | '''Snapshot our relation information into local state. |
954 | |
955 | @@ -1299,10 +1108,24 @@ |
956 | # slave replication-relation-changed (noop; slave not yet joined db rel) |
957 | # slave db-relation-joined (republish) |
958 | |
959 | -def db_relation_joined_changed(user, database, roles): |
960 | - if local_state['state'] not in ('master', 'standalone'): |
961 | +@hooks.hook('db-relation-joined', 'db-relation-changed') |
962 | +def db_relation_joined_changed(): |
963 | + if local_state['state'] == 'hot standby': |
964 | + publish_hot_standby_credentials() |
965 | return |
966 | |
967 | + # By default, we create a database named after the remote |
968 | + # servicename. The remote service can override this by setting |
969 | + # the database property on the relation. |
970 | + database = hookenv.relation_get('database') |
971 | + if not database: |
972 | + database = hookenv.remote_unit().split('/')[0] |
973 | + |
974 | + # Generate a unique username for this relation to use. |
975 | + user = user_name(hookenv.relation_id(), hookenv.remote_unit()) |
976 | + |
977 | + roles = filter(None, (hookenv.relation_get('roles') or '').split(",")) |
978 | + |
979 | log('{} unit publishing credentials'.format(local_state['state'])) |
980 | |
981 | password = create_user(user) |
982 | @@ -1310,8 +1133,8 @@ |
983 | schema_user = "{}_schema".format(user) |
984 | schema_password = create_user(schema_user) |
985 | ensure_database(user, schema_user, database) |
986 | - host = get_unit_host() |
987 | - port = config_get()["listen_port"] |
988 | + host = hookenv.unit_private_ip() |
989 | + port = hookenv.config('listen_port') |
990 | state = local_state['state'] # master, hot standby, standalone |
991 | |
992 | # Publish connection details. |
993 | @@ -1336,15 +1159,20 @@ |
994 | snapshot_relations() |
995 | |
996 | |
997 | -def db_admin_relation_joined_changed(user): |
998 | - if local_state['state'] not in ('master', 'standalone'): |
999 | +@hooks.hook('db-admin-relation-joined', 'db-admin-relation-changed') |
1000 | +def db_admin_relation_joined_changed(): |
1001 | + if local_state['state'] == 'hot standby': |
1002 | + publish_hot_standby_credentials() |
1003 | return |
1004 | |
1005 | + user = user_name( |
1006 | + hookenv.relation_id(), hookenv.remote_unit(), admin=True) |
1007 | + |
1008 | log('{} unit publishing credentials'.format(local_state['state'])) |
1009 | |
1010 | password = create_user(user, admin=True) |
1011 | - host = get_unit_host() |
1012 | - port = config_get()["listen_port"] |
1013 | + host = hookenv.unit_private_ip() |
1014 | + port = hookenv.config('listen_port') |
1015 | state = local_state['state'] # master, hot standby, standalone |
1016 | |
1017 | # Publish connection details. |
1018 | @@ -1366,6 +1194,7 @@ |
1019 | snapshot_relations() |
1020 | |
1021 | |
1022 | +@hooks.hook() |
1023 | def db_relation_broken(): |
1024 | from psycopg2.extensions import AsIs |
1025 | |
1026 | @@ -1398,6 +1227,7 @@ |
1027 | snapshot_relations() |
1028 | |
1029 | |
1030 | +@hooks.hook() |
1031 | def db_admin_relation_broken(): |
1032 | from psycopg2.extensions import AsIs |
1033 | |
1034 | @@ -1413,12 +1243,8 @@ |
1035 | snapshot_relations() |
1036 | |
1037 | |
1038 | -def TODO(msg): |
1039 | - juju_log(MSG_WARNING, 'TODO> %s' % msg) |
1040 | - |
1041 | - |
1042 | def add_extra_repos(): |
1043 | - extra_repos = config_get('extra_archives') |
1044 | + extra_repos = hookenv.config('extra_archives') |
1045 | extra_repos_added = local_state.setdefault('extra_repos_added', set()) |
1046 | if extra_repos: |
1047 | repos_added = False |
1048 | @@ -1428,7 +1254,7 @@ |
1049 | extra_repos_added.add(repo) |
1050 | repos_added = True |
1051 | if repos_added: |
1052 | - run('apt-get update') |
1053 | + host.apt_update(fatal=True) |
1054 | local_state.save() |
1055 | |
1056 | |
1057 | @@ -1441,7 +1267,7 @@ |
1058 | """ |
1059 | comment = 'repmgr key for {}'.format(os.environ['JUJU_UNIT_NAME']) |
1060 | if not os.path.isdir(postgres_ssh_dir): |
1061 | - install_dir(postgres_ssh_dir, "postgres", "postgres", 0700) |
1062 | + host.mkdir(postgres_ssh_dir, "postgres", "postgres", 0o700) |
1063 | if not os.path.exists(postgres_ssh_private_key): |
1064 | run("sudo -u postgres -H ssh-keygen -q -t rsa -C '{}' -N '' " |
1065 | "-f '{}'".format(comment, postgres_ssh_private_key)) |
1066 | @@ -1457,9 +1283,9 @@ |
1067 | authorized_units = set() |
1068 | authorized_keys = set() |
1069 | known_hosts = set() |
1070 | - for relid in relation_ids(relation_types=replication_relation_types): |
1071 | - for unit in relation_list(relid): |
1072 | - relation = relation_get(unit_name=unit, relation_id=relid) |
1073 | + for relid in hookenv.relation_ids('replication'): |
1074 | + for unit in hookenv.related_units(relid): |
1075 | + relation = hookenv.relation_get(unit=unit, rid=relid) |
1076 | public_key = relation.get('public_ssh_key', None) |
1077 | if public_key: |
1078 | authorized_units.add(unit) |
1079 | @@ -1468,14 +1294,14 @@ |
1080 | relation['private-address'], relation['ssh_host_key'])) |
1081 | |
1082 | # Generate known_hosts |
1083 | - install_file( |
1084 | - '\n'.join(known_hosts), postgres_ssh_known_hosts, |
1085 | - owner="postgres", group="postgres", mode=0o644) |
1086 | + write_file( |
1087 | + postgres_ssh_known_hosts, '\n'.join(known_hosts), |
1088 | + owner="postgres", group="postgres", perms=0o644) |
1089 | |
1090 | # Generate authorized_keys |
1091 | - install_file( |
1092 | - '\n'.join(authorized_keys), postgres_ssh_authorized_keys, |
1093 | - owner="postgres", group="postgres", mode=0o400) |
1094 | + write_file( |
1095 | + postgres_ssh_authorized_keys, '\n'.join(authorized_keys), |
1096 | + owner="postgres", group="postgres", perms=0o400) |
1097 | |
1098 | # Publish details, so relation knows they have been granted access. |
1099 | local_state['authorized'] = authorized_units |
1100 | @@ -1495,9 +1321,9 @@ |
1101 | pgpass = '\n'.join( |
1102 | "*:*:*:{}:{}".format(username, password) |
1103 | for username, password in passwords.items()) |
1104 | - install_file( |
1105 | - pgpass, charm_pgpass, |
1106 | - owner="postgres", group="postgres", mode=0o400) |
1107 | + write_file( |
1108 | + charm_pgpass, pgpass, |
1109 | + owner="postgres", group="postgres", perms=0o400) |
1110 | |
1111 | |
1112 | def drop_database(dbname, warn=True): |
1113 | @@ -1511,8 +1337,7 @@ |
1114 | except psycopg2.Error: |
1115 | if time.time() > now + timeout: |
1116 | if warn: |
1117 | - juju_log( |
1118 | - MSG_WARNING, "Unable to drop database %s" % dbname) |
1119 | + log("Unable to drop database {}".format(dbname), WARNING) |
1120 | else: |
1121 | raise |
1122 | time.sleep(0.5) |
1123 | @@ -1542,26 +1367,9 @@ |
1124 | def follow_database(master): |
1125 | '''Connect the database as a streaming replica of the master.''' |
1126 | master_relation = hookenv.relation_get(unit=master) |
1127 | - |
1128 | - recovery_conf_path = os.path.join(postgresql_cluster_dir, 'recovery.conf') |
1129 | - if os.path.exists(recovery_conf_path): |
1130 | - old_recovery_conf = open(recovery_conf_path, 'r').read() |
1131 | - else: |
1132 | - old_recovery_conf = None |
1133 | - |
1134 | - recovery_conf = Template( |
1135 | - open("templates/recovery.conf.tmpl").read()).render({ |
1136 | - 'host': master_relation['private-address'], |
1137 | - 'password': local_state['replication_password']}) |
1138 | - juju_log(MSG_DEBUG, recovery_conf) |
1139 | - install_file( |
1140 | - recovery_conf, |
1141 | - os.path.join(postgresql_cluster_dir, 'recovery.conf'), |
1142 | - owner="postgres", group="postgres") |
1143 | - |
1144 | - if recovery_conf != old_recovery_conf: |
1145 | - log("recovery.conf updated. Restarting to take effect.") |
1146 | - postgresql_restart() |
1147 | + create_recovery_conf( |
1148 | + master_relation['private-address'], |
1149 | + local_state['replication_password'], restart_on_change=True) |
1150 | |
1151 | |
1152 | def elected_master(): |
1153 | @@ -1631,8 +1439,9 @@ |
1154 | return master |
1155 | |
1156 | |
1157 | +@hooks.hook('replication-relation-joined', 'replication-relation-changed') |
1158 | def replication_relation_joined_changed(): |
1159 | - config_changed(postgresql_config) # Ensure minimal replication settings. |
1160 | + config_changed() # Ensure minimal replication settings. |
1161 | |
1162 | # Now that pg_hba.conf has been regenerated and loaded, inform related |
1163 | # units that they have been granted replication access. |
1164 | @@ -1724,11 +1533,11 @@ |
1165 | def publish_hot_standby_credentials(): |
1166 | ''' |
1167 | If a hot standby joins a client relation before the master |
1168 | - unit, it was unable to publish connection details. However, |
1169 | + unit, it is unable to publish connection details. However, |
1170 | when the master does join it updates the client_relations |
1171 | - value in the peer relation causing the |
1172 | - replication-relation-changed hook to be invoked. This gives us |
1173 | - a second opertunity to publish connection details. |
1174 | + value in the peer relation causing the replication-relation-changed |
1175 | + hook to be invoked. This gives us a second opertunity to publish |
1176 | + connection details. |
1177 | |
1178 | This function is invoked from both the client and peer |
1179 | relation-changed hook. One of these will work depending on the order |
1180 | @@ -1737,7 +1546,7 @@ |
1181 | master = local_state['following'] |
1182 | |
1183 | client_relations = hookenv.relation_get( |
1184 | - 'client_relations', master, relation_ids('replication')[0]) |
1185 | + 'client_relations', master, hookenv.relation_ids('replication')[0]) |
1186 | |
1187 | if client_relations is None: |
1188 | log("Master {} has not yet joined any client relations".format( |
1189 | @@ -1763,8 +1572,8 @@ |
1190 | unit=master, rid=client_relation) |
1191 | |
1192 | # Override unit specific connection details |
1193 | - connection_settings['host'] = get_unit_host() |
1194 | - connection_settings['port'] = config_get()["listen_port"] |
1195 | + connection_settings['host'] = hookenv.unit_private_ip() |
1196 | + connection_settings['port'] = hookenv.config('listen_port') |
1197 | connection_settings['state'] = local_state['state'] |
1198 | |
1199 | # Block until users and database has replicated, so we know the |
1200 | @@ -1787,11 +1596,10 @@ |
1201 | client_relation, relation_settings=connection_settings) |
1202 | |
1203 | |
1204 | +@hooks.hook() |
1205 | def replication_relation_departed(): |
1206 | '''A unit has left the replication peer group.''' |
1207 | remote_unit = hookenv.remote_unit() |
1208 | - remote_relation = hookenv.relation_get() |
1209 | - remote_state = remote_relation['state'] |
1210 | |
1211 | assert remote_unit is not None |
1212 | |
1213 | @@ -1839,32 +1647,33 @@ |
1214 | if 'paused_at_failover' in local_state: |
1215 | del local_state['paused_at_failover'] |
1216 | |
1217 | - config_changed(postgresql_config) |
1218 | + config_changed() |
1219 | local_state.publish() |
1220 | |
1221 | |
1222 | +@hooks.hook() |
1223 | def replication_relation_broken(): |
1224 | # This unit has been removed from the service. |
1225 | promote_database() |
1226 | if os.path.exists(charm_pgpass): |
1227 | os.unlink(charm_pgpass) |
1228 | - config_changed(postgresql_config) |
1229 | + config_changed() |
1230 | |
1231 | |
1232 | def clone_database(master_unit, master_host): |
1233 | postgresql_stop() |
1234 | - juju_log(MSG_INFO, "Cloning master {}".format(master_unit)) |
1235 | + log("Cloning master {}".format(master_unit)) |
1236 | |
1237 | cmd = ['sudo', '-E', '-u', 'postgres', # -E needed to locate pgpass file. |
1238 | 'pg_basebackup', '-D', postgresql_cluster_dir, |
1239 | '--xlog', '--checkpoint=fast', '--no-password', |
1240 | '-h', master_host, '-p', '5432', '--username=juju_replication'] |
1241 | - juju_log(MSG_DEBUG, ' '.join(cmd)) |
1242 | + log(' '.join(cmd), DEBUG) |
1243 | if os.path.isdir(postgresql_cluster_dir): |
1244 | shutil.rmtree(postgresql_cluster_dir) |
1245 | try: |
1246 | output = subprocess.check_output(cmd) |
1247 | - juju_log(MSG_DEBUG, output) |
1248 | + log(output, DEBUG) |
1249 | # Debian by default expects SSL certificates in the datadir. |
1250 | os.symlink( |
1251 | '/etc/ssl/certs/ssl-cert-snakeoil.pem', |
1252 | @@ -1872,29 +1681,21 @@ |
1253 | os.symlink( |
1254 | '/etc/ssl/private/ssl-cert-snakeoil.key', |
1255 | os.path.join(postgresql_cluster_dir, 'server.key')) |
1256 | - recovery_conf = Template( |
1257 | - open("templates/recovery.conf.tmpl").read()).render({ |
1258 | - 'host': master_host, |
1259 | - 'password': local_state['replication_password']}) |
1260 | - juju_log(MSG_DEBUG, recovery_conf) |
1261 | - install_file( |
1262 | - recovery_conf, |
1263 | - os.path.join(postgresql_cluster_dir, 'recovery.conf'), |
1264 | - owner="postgres", group="postgres") |
1265 | + create_recovery_conf(master_host, local_state['replication_password']) |
1266 | except subprocess.CalledProcessError, x: |
1267 | # We failed, and this cluster is broken. Rebuild a |
1268 | # working cluster so start/stop etc. works and we |
1269 | # can retry hooks again. Even assuming the charm is |
1270 | # functioning correctly, the clone may still fail |
1271 | # due to eg. lack of disk space. |
1272 | - juju_log(MSG_ERROR, "Clone failed, db cluster destroyed") |
1273 | - juju_log(MSG_ERROR, x.output) |
1274 | + log("Clone failed, db cluster destroyed", ERROR) |
1275 | + log(x.output, ERROR) |
1276 | if os.path.exists(postgresql_cluster_dir): |
1277 | shutil.rmtree(postgresql_cluster_dir) |
1278 | if os.path.exists(postgresql_config_dir): |
1279 | shutil.rmtree(postgresql_config_dir) |
1280 | run('pg_createcluster {} main'.format(version)) |
1281 | - config_changed(postgresql_config) |
1282 | + config_changed() |
1283 | raise |
1284 | finally: |
1285 | postgresql_start() |
1286 | @@ -1903,8 +1704,8 @@ |
1287 | |
1288 | def slave_count(): |
1289 | num_slaves = 0 |
1290 | - for relid in relation_ids(relation_types=replication_relation_types): |
1291 | - num_slaves += len(relation_list(relid)) |
1292 | + for relid in hookenv.relation_ids('replication'): |
1293 | + num_slaves += len(hookenv.related_units(relid)) |
1294 | return num_slaves |
1295 | |
1296 | |
1297 | @@ -1949,8 +1750,9 @@ |
1298 | units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1]))) |
1299 | |
1300 | |
1301 | +@hooks.hook('nrpe-external-master-relation-changed') |
1302 | def update_nrpe_checks(): |
1303 | - config_data = config_get() |
1304 | + config_data = hookenv.config() |
1305 | try: |
1306 | nagios_uid = getpwnam('nagios').pw_uid |
1307 | nagios_gid = getgrnam('nagios').gr_gid |
1308 | @@ -1958,7 +1760,7 @@ |
1309 | hookenv.log("Nagios user not set up.", hookenv.DEBUG) |
1310 | return |
1311 | |
1312 | - unit_name = os.environ['JUJU_UNIT_NAME'].replace('/', '-') |
1313 | + unit_name = hookenv.local_unit().replace('/', '-') |
1314 | nagios_hostname = "%s-%s" % (config_data['nagios_context'], unit_name) |
1315 | nagios_logdir = '/var/log/nagios' |
1316 | nrpe_service_file = \ |
1317 | @@ -2009,10 +1811,11 @@ |
1318 | if os.path.isfile('/etc/init.d/nagios-nrpe-server'): |
1319 | subprocess.call(['service', 'nagios-nrpe-server', 'reload']) |
1320 | |
1321 | + |
1322 | ############################################################################### |
1323 | # Global variables |
1324 | ############################################################################### |
1325 | -config_data = config_get() |
1326 | +config_data = hookenv.config() |
1327 | version = config_data['version'] |
1328 | cluster_name = config_data['cluster_name'] |
1329 | postgresql_data_dir = "/var/lib/postgresql" |
1330 | @@ -2046,10 +1849,7 @@ |
1331 | os.environ['PGPASSFILE'] = charm_pgpass |
1332 | |
1333 | |
1334 | -############################################################################### |
1335 | -# Main section |
1336 | -############################################################################### |
1337 | -def main(): |
1338 | +if __name__ == '__main__': |
1339 | # Hook and context overview. The various replication and client |
1340 | # hooks interact in complex ways. |
1341 | log("Running {} hook".format(hook_name)) |
1342 | @@ -2057,90 +1857,4 @@ |
1343 | log("Relation {} with {}".format( |
1344 | hookenv.relation_id(), hookenv.remote_unit())) |
1345 | |
1346 | - if hook_name == "install": |
1347 | - install() |
1348 | - |
1349 | - elif hook_name == "config-changed": |
1350 | - config_changed(postgresql_config) |
1351 | - |
1352 | - elif hook_name == "upgrade-charm": |
1353 | - install(run_pre=False) |
1354 | - upgrade_charm() |
1355 | - |
1356 | - elif hook_name == "start": |
1357 | - if not postgresql_restart(): |
1358 | - raise SystemExit(1) |
1359 | - |
1360 | - elif hook_name == "stop": |
1361 | - if not postgresql_stop(): |
1362 | - raise SystemExit(1) |
1363 | - |
1364 | - elif hook_name == "db-relation-joined": |
1365 | - # By default, we create a database named after the remote |
1366 | - # servicename. The remote service can override this by setting |
1367 | - # the database property on the relation. |
1368 | - database = os.environ['JUJU_REMOTE_UNIT'].split('/')[0] |
1369 | - |
1370 | - # Generate a unique username for this relation to use. |
1371 | - user = user_name( |
1372 | - os.environ['JUJU_RELATION_ID'], os.environ['JUJU_REMOTE_UNIT']) |
1373 | - |
1374 | - db_relation_joined_changed(user, database, []) # No roles yet. |
1375 | - |
1376 | - elif hook_name == "db-relation-changed": |
1377 | - roles = filter(None, (relation_get('roles') or '').split(",")) |
1378 | - |
1379 | - # If the remote service has requested we use a particular database |
1380 | - # name, honour that request. |
1381 | - database = relation_get('database') |
1382 | - if not database: |
1383 | - database = relation_get('database', os.environ['JUJU_UNIT_NAME']) |
1384 | - |
1385 | - user = relation_get('user', os.environ['JUJU_UNIT_NAME']) |
1386 | - if not user: |
1387 | - user = user_name( |
1388 | - os.environ['JUJU_RELATION_ID'], os.environ['JUJU_REMOTE_UNIT']) |
1389 | - db_relation_joined_changed(user, database, roles) |
1390 | - |
1391 | - elif hook_name == "db-relation-broken": |
1392 | - db_relation_broken() |
1393 | - |
1394 | - elif hook_name in ("db-admin-relation-joined", |
1395 | - "db-admin-relation-changed"): |
1396 | - user = user_name(os.environ['JUJU_RELATION_ID'], |
1397 | - os.environ['JUJU_REMOTE_UNIT'], admin=True) |
1398 | - db_admin_relation_joined_changed(user) |
1399 | - |
1400 | - elif hook_name == "db-admin-relation-broken": |
1401 | - db_admin_relation_broken() |
1402 | - |
1403 | - elif hook_name == "nrpe-external-master-relation-changed": |
1404 | - update_nrpe_checks() |
1405 | - |
1406 | - elif hook_name == 'replication-relation-joined': |
1407 | - replication_relation_joined_changed() |
1408 | - |
1409 | - elif hook_name == 'replication-relation-changed': |
1410 | - replication_relation_joined_changed() |
1411 | - |
1412 | - elif hook_name == 'replication-relation-departed': |
1413 | - replication_relation_departed() |
1414 | - |
1415 | - elif hook_name == 'replication-relation-broken': |
1416 | - replication_relation_broken() |
1417 | - |
1418 | - #-------- persistent-storage-relation-joined, |
1419 | - # persistent-storage-relation-changed |
1420 | - #elif hook_name in ["persistent-storage-relation-joined", |
1421 | - # "persistent-storage-relation-changed"]: |
1422 | - # persistent_storage_relation_joined_changed() |
1423 | - #-------- persistent-storage-relation-broken |
1424 | - #elif hook_name == "persistent-storage-relation-broken": |
1425 | - # persistent_storage_relation_broken() |
1426 | - else: |
1427 | - print "Unknown hook {}".format(hook_name) |
1428 | - raise SystemExit(1) |
1429 | - |
1430 | - |
1431 | -if __name__ == '__main__': |
1432 | - raise SystemExit(main()) |
1433 | + hooks.execute(sys.argv) |
1434 | |
1435 | === modified file 'test.py' |
1436 | --- test.py 2013-07-08 11:07:29 +0000 |
1437 | +++ test.py 2013-07-08 11:07:29 +0000 |
1438 | @@ -255,7 +255,7 @@ |
1439 | _run(self, cmd) |
1440 | |
1441 | def test_basic(self): |
1442 | - '''Set up a single unit service''' |
1443 | + '''Connect to a a single unit service via the db relationship.''' |
1444 | self.juju.deploy(TEST_CHARM, 'postgresql') |
1445 | self.juju.deploy(PSQL_CHARM, 'psql') |
1446 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1447 | @@ -265,10 +265,20 @@ |
1448 | # from adding the relation. I'm protected here as 'juju status' |
1449 | # takes about 25 seconds to run from here to my test cloud but |
1450 | # others might not be so 'lucky'. |
1451 | - self.addDetail('status', text_content(repr(self.juju.status))) |
1452 | result = self.sql('SELECT TRUE') |
1453 | self.assertEqual(result, [['t']]) |
1454 | |
1455 | + def test_basic_admin(self): |
1456 | + '''Connect to a single unit service via the db-admin relationship.''' |
1457 | + self.juju.deploy(TEST_CHARM, 'postgresql') |
1458 | + self.juju.deploy(PSQL_CHARM, 'psql') |
1459 | + self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin']) |
1460 | + self.juju.wait_until_ready() |
1461 | + |
1462 | + result = self.sql('SELECT TRUE', dbname='postgres') |
1463 | + self.assertEqual(result, [['t']]) |
1464 | + |
1465 | + |
1466 | def is_master(self, postgres_unit, dbname=None): |
1467 | is_master = self.sql( |
1468 | 'SELECT NOT pg_is_in_recovery()', |
sweet! looks good... love removing code.