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: | |||
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.