~stub/charms/precise/postgresql/bug-1302494-fix-persistent-storage

« back to all changes in this revision

Viewing changes to hooks/hooks.py

  • Committer: Marco Ceppi
  • Date: 2014-02-13 12:53:42 UTC
  • mfrom: (46.10.42 pg93)
  • Revision ID: marco@ceppi.net-20140213125342-8sag93bok4kxg6eq
[stub] Run tests for all supported versions of PostgreSQL.

Show diffs side-by-side

added added

removed removed

Lines of Context:
60
60
    package candidate, saving it in local_state for later.
61
61
    '''
62
62
    config_data = hookenv.config()
63
 
    if config_data['version']:
 
63
    if 'pg_version' in local_state:
 
64
        version = local_state['pg_version']
 
65
    elif 'version' in config_data:
64
66
        version = config_data['version']
65
 
    elif 'pg_version' in local_state:
66
 
        version = local_state['pg_version']
67
67
    else:
68
68
        log("map version from distro release ...")
69
 
        distro_release = run("lsb_release -sc")
70
 
        distro_release = distro_release.rstrip()
71
69
        version_map = {'precise': '9.1',
72
70
                       'trusty': '9.3'}
73
 
        version = version_map.get(distro_release)
 
71
        version = version_map.get(distro_codename())
74
72
        if not version:
75
 
            log("No PG version map for distro_release={}, "
76
 
                "you'll need to explicitly set it".format(distro_release),
 
73
            log("No PG version map for distro_codename={}, "
 
74
                "you'll need to explicitly set it".format(distro_codename()),
77
75
                CRITICAL)
78
76
            sys.exit(1)
79
 
        log("version={} from distro_release='{}'".format(
80
 
            version, distro_release))
 
77
        log("version={} from distro_codename='{}'".format(
 
78
            version, distro_codename()))
81
79
        # save it for later
82
80
        local_state.setdefault('pg_version', version)
83
81
        local_state.save()
86
84
    return version
87
85
 
88
86
 
 
87
def distro_codename():
 
88
    """Return the distro release code name, eg. 'precise' or 'trusty'."""
 
89
    return host.lsb_release()['DISTRIB_CODENAME']
 
90
 
 
91
 
89
92
class State(dict):
90
93
    """Encapsulate state common to the unit for republishing to relations."""
91
94
    def __init__(self, state_file):
128
131
        replication_state = dict(client_state)
129
132
 
130
133
        add(replication_state, 'replication_password')
 
134
        add(replication_state, 'port')
131
135
        add(replication_state, 'wal_received_offset')
132
136
        add(replication_state, 'following')
133
137
        add(replication_state, 'client_relations')
242
246
        startup_file, contents, 'postgres', 'postgres', perms=0o644)
243
247
 
244
248
 
245
 
def run(command, exit_on_error=True):
 
249
def run(command, exit_on_error=True, quiet=False):
246
250
    '''Run a command and return the output.'''
247
 
    try:
248
 
        log(command, DEBUG)
249
 
        return subprocess.check_output(
250
 
            command, stderr=subprocess.STDOUT, shell=True)
251
 
    except subprocess.CalledProcessError, e:
252
 
        log("status=%d, output=%s" % (e.returncode, e.output), ERROR)
253
 
        if exit_on_error:
254
 
            sys.exit(e.returncode)
255
 
        else:
256
 
            raise
 
251
    log("Running {!r}".format(command), DEBUG)
 
252
    p = subprocess.Popen(
 
253
        command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
 
254
        shell=isinstance(command, basestring))
 
255
    p.stdin.close()
 
256
    lines = []
 
257
    for line in p.stdout:
 
258
        if line:
 
259
            # LP:1274460 & LP:1259490 mean juju-log is no where near as
 
260
            # useful as we would like, so just shove a copy of the
 
261
            # output to stdout for logging.
 
262
            # log("> {}".format(line), DEBUG)
 
263
            if not quiet:
 
264
                print line
 
265
            lines.append(line)
 
266
        elif p.poll() is not None:
 
267
            break
 
268
 
 
269
    p.wait()
 
270
 
 
271
    if p.returncode == 0:
 
272
        return '\n'.join(lines)
 
273
 
 
274
    if p.returncode != 0 and exit_on_error:
 
275
        log("ERROR: {}".format(p.returncode), ERROR)
 
276
        sys.exit(p.returncode)
 
277
 
 
278
    raise subprocess.CalledProcessError(
 
279
        p.returncode, command, '\n'.join(lines))
257
280
 
258
281
 
259
282
def postgresql_is_running():
260
283
    '''Return true if PostgreSQL is running.'''
261
 
    # init script always return true (9.1), add extra check to make it useful
262
 
    status, output = commands.getstatusoutput("invoke-rc.d postgresql status")
263
 
    if status != 0:
264
 
        return False
265
 
    # e.g. output: "Running clusters: 9.1/main"
266
 
    vc = "%s/%s" % (pg_version(), hookenv.config("cluster_name"))
267
 
    return vc in output.decode('utf8').split()
 
284
    for version, name, _, status in lsclusters(slice(4)):
 
285
        if (version, name) == (pg_version(), hookenv.config('cluster_name')):
 
286
            if 'online' in status.split(','):
 
287
                log('PostgreSQL is running', DEBUG)
 
288
                return True
 
289
            else:
 
290
                log('PostgreSQL is not running', DEBUG)
 
291
                return False
 
292
    assert False, 'Cluster {} {} not found'.format(
 
293
        pg_version(), hookenv.config('cluster_name'))
268
294
 
269
295
 
270
296
def postgresql_stop():
271
297
    '''Shutdown PostgreSQL.'''
272
 
    success = host.service_stop('postgresql')
273
 
    return not (success and postgresql_is_running())
 
298
    if postgresql_is_running():
 
299
        run([
 
300
            'pg_ctlcluster', '--force',
 
301
            pg_version(), hookenv.config('cluster_name'), 'stop'])
 
302
        log('PostgreSQL shut down')
274
303
 
275
304
 
276
305
def postgresql_start():
277
306
    '''Start PostgreSQL if it is not already running.'''
278
 
    success = host.service_start('postgresql')
279
 
    return success and postgresql_is_running()
 
307
    if not postgresql_is_running():
 
308
        run([
 
309
            'pg_ctlcluster', pg_version(),
 
310
            hookenv.config('cluster_name'), 'start'])
 
311
        log('PostgreSQL started')
280
312
 
281
313
 
282
314
def postgresql_restart():
283
315
    '''Restart PostgreSQL, or start it if it is not already running.'''
284
316
    if postgresql_is_running():
285
317
        with restart_lock(hookenv.local_unit(), True):
286
 
            # 'service postgresql restart' fails; it only does a reload.
287
 
            # success = host.service_restart('postgresql')
288
 
            try:
289
 
                run('pg_ctlcluster -force {} {} '
290
 
                    'restart'.format(pg_version(),
291
 
                                     hookenv.config('cluster_name')))
292
 
                success = True
293
 
            except subprocess.CalledProcessError:
294
 
                success = False
 
318
            run([
 
319
                'pg_ctlcluster', '--force',
 
320
                pg_version(), hookenv.config('cluster_name'), 'restart'])
 
321
            log('PostgreSQL restarted')
295
322
    else:
296
 
        success = host.service_start('postgresql')
 
323
        postgresql_start()
 
324
 
 
325
    assert postgresql_is_running()
297
326
 
298
327
    # Store a copy of our known live configuration so
299
328
    # postgresql_reload_or_restart() can make good choices.
300
 
    if success and 'saved_config' in local_state:
 
329
    if 'saved_config' in local_state:
301
330
        local_state['live_config'] = local_state['saved_config']
302
331
        local_state.save()
303
332
 
304
 
    return success and postgresql_is_running()
305
 
 
306
333
 
307
334
def postgresql_reload():
308
335
    '''Make PostgreSQL reload its configuration.'''
309
336
    # reload returns a reliable exit status
310
 
    status, output = commands.getstatusoutput("invoke-rc.d postgresql reload")
311
 
    return (status == 0)
 
337
    if postgresql_is_running():
 
338
        # I'm using the PostgreSQL function to avoid as much indirection
 
339
        # as possible.
 
340
        success = run_select_as_postgres('SELECT pg_reload_conf()')[1][0][0]
 
341
        assert success, 'Failed to reload PostgreSQL configuration'
 
342
        log('PostgreSQL configuration reloaded')
 
343
    return postgresql_start()
312
344
 
313
345
 
314
346
def requires_restart():
340
372
                # A setting has changed that requires PostgreSQL to be
341
373
                # restarted before it will take effect.
342
374
                restart = True
 
375
                log('{} changed from {} to {}. Restart required.'.format(
 
376
                    name, live_value, new_value), DEBUG)
343
377
    return restart
344
378
 
345
379
 
346
380
def postgresql_reload_or_restart():
347
381
    """Reload PostgreSQL configuration, restarting if necessary."""
348
382
    if requires_restart():
349
 
        log("Configuration change requires PostgreSQL restart. Restarting.",
350
 
            WARNING)
351
 
        success = postgresql_restart()
352
 
        if not success or requires_restart():
353
 
            log("Configuration changes failed to apply", WARNING)
354
 
            success = False
 
383
        log("Configuration change requires PostgreSQL restart", WARNING)
 
384
        postgresql_restart()
 
385
        assert not requires_restart(), "Configuration changes failed to apply"
355
386
    else:
356
 
        success = host.service_reload('postgresql')
357
 
 
358
 
    if success:
359
 
        local_state['saved_config'] = local_state['live_config']
360
 
        local_state.save()
361
 
 
362
 
    return success
363
 
 
364
 
 
365
 
def get_service_port(config_file):
 
387
        postgresql_reload()
 
388
 
 
389
    local_state['saved_config'] = local_state['live_config']
 
390
    local_state.save()
 
391
 
 
392
 
 
393
def get_service_port():
366
394
    '''Return the port PostgreSQL is listening on.'''
367
 
    if not os.path.exists(config_file):
368
 
        return None
369
 
    postgresql_config = open(config_file, 'r').read()
370
 
    port = re.search("port.*=(.*)", postgresql_config).group(1).strip()
371
 
    try:
372
 
        return int(port)
373
 
    except (ValueError, TypeError):
374
 
        return None
 
395
    for version, name, port in lsclusters(slice(3)):
 
396
        if (version, name) == (pg_version(), hookenv.config('cluster_name')):
 
397
            return int(port)
 
398
 
 
399
    assert False, 'No port found for {!r} {!r}'.format(
 
400
        pg_version(), hookenv.config['cluster_name'])
 
401
 
 
402
 
 
403
def lsclusters(s=slice(0, -1)):
 
404
    for line in run('pg_lsclusters', quiet=True).splitlines()[1:]:
 
405
        if line:
 
406
            yield line.split()[s]
375
407
 
376
408
 
377
409
def _get_system_ram():
388
420
def create_postgresql_config(config_file):
389
421
    '''Create the postgresql.conf file'''
390
422
    config_data = hookenv.config()
 
423
    if not config_data.get('listen_port', None):
 
424
        config_data['listen_port'] = get_service_port()
391
425
    if config_data["performance_tuning"] == "auto":
392
426
        # Taken from:
393
427
        # http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server
439
473
    # Return it as pg_config
440
474
    charm_dir = hookenv.charm_dir()
441
475
    template_file = "{}/templates/postgresql.conf.tmpl".format(charm_dir)
442
 
    if not config_data['version']:
 
476
    if not config_data.get('version', None):
443
477
        config_data['version'] = pg_version()
444
478
    pg_config = Template(
445
479
        open(template_file).read()).render(config_data)
615
649
    host.write_file(output_file, crontab_template, perms=0600)
616
650
 
617
651
 
618
 
def create_recovery_conf(master_host, restart_on_change=False):
 
652
def create_recovery_conf(master_host, master_port, restart_on_change=False):
619
653
    version = pg_version()
620
654
    cluster_name = hookenv.config('cluster_name')
621
655
    postgresql_cluster_dir = os.path.join(
632
666
    template_file = "{}/templates/recovery.conf.tmpl".format(charm_dir)
633
667
    recovery_conf = Template(open(template_file).read()).render({
634
668
        'host': master_host,
 
669
        'port': master_port,
635
670
        'password': local_state['replication_password'],
636
671
        'streaming_replication': streaming_replication})
637
672
    log(recovery_conf, DEBUG)
657
692
        return(None)
658
693
 
659
694
 
660
 
#------------------------------------------------------------------------------
661
 
# update_service_ports:  Convenience function that evaluate the old and new
662
 
#                        service ports to decide which ports need to be
663
 
#                        opened and which to close
664
 
#------------------------------------------------------------------------------
665
 
def update_service_port(old_service_port=None, new_service_port=None):
666
 
    if old_service_port is None or new_service_port is None:
667
 
        return(None)
668
 
    if new_service_port != old_service_port:
669
 
        hookenv.close_port(old_service_port)
670
 
        hookenv.open_port(new_service_port)
 
695
def update_service_port():
 
696
    old_port = local_state.get('listen_port', None)
 
697
    new_port = get_service_port()
 
698
    if old_port != new_port:
 
699
        if new_port:
 
700
            hookenv.open_port(new_port)
 
701
        if old_port:
 
702
            hookenv.close_port(old_port)
 
703
        local_state['listen_port'] = new_port
 
704
        local_state.save()
671
705
 
672
706
 
673
707
def create_ssl_cert(cluster_dir):
702
736
 
703
737
 
704
738
def db_cursor(autocommit=False, db='postgres', user='postgres',
705
 
              host=None, timeout=30):
 
739
              host=None, port=None, timeout=30):
706
740
    import psycopg2
 
741
    if port is None:
 
742
        port = get_service_port()
707
743
    if host:
708
 
        conn_str = "dbname={} host={} user={}".format(db, host, user)
 
744
        conn_str = "dbname={} host={} port={} user={}".format(
 
745
            db, host, port, user)
709
746
    else:
710
 
        conn_str = "dbname={} user={}".format(db, user)
 
747
        conn_str = "dbname={} port={} user={}".format(db, port, user)
711
748
    # There are often race conditions in opening database connections,
712
749
    # such as a reload having just happened to change pg_hba.conf
713
750
    # settings or a hot standby being restarted and needing to catch up
750
787
    return (cur.rowcount, results)
751
788
 
752
789
 
 
790
def validate_config():
 
791
    """
 
792
    Sanity check charm configuration, aborting the script if
 
793
    we have bogus config values or config changes the charm does not yet
 
794
    (or cannot) support.
 
795
    """
 
796
    valid = True
 
797
    config_data = hookenv.config()
 
798
 
 
799
    version = config_data.get('version', None)
 
800
    if version:
 
801
        if version not in ('9.1', '9.2', '9.3'):
 
802
            valid = False
 
803
            log("Invalid or unsupported version {!r} requested".format(
 
804
                version), CRITICAL)
 
805
 
 
806
    if config_data['cluster_name'] != 'main':
 
807
        valid = False
 
808
        log("Cluster names other than 'main' do not work per LP:1271835",
 
809
            CRITICAL)
 
810
 
 
811
    if config_data['listen_ip'] != '*':
 
812
        valid = False
 
813
        log("listen_ip values other than '*' do not work per LP:1271837",
 
814
            CRITICAL)
 
815
 
 
816
    unchangeable_config = [
 
817
        'locale', 'encoding', 'version', 'cluster_name', 'pgdg']
 
818
 
 
819
    for name in unchangeable_config:
 
820
        if (name in local_state
 
821
                and local_state[name] != config_data.get(name, None)):
 
822
            valid = False
 
823
            log("Cannot change {!r} setting after install.".format(name))
 
824
        local_state[name] = config_data.get(name, None)
 
825
    local_state.save()
 
826
 
 
827
    if not valid:
 
828
        sys.exit(99)
 
829
 
 
830
 
753
831
#------------------------------------------------------------------------------
754
832
# Core logic for permanent storage changes:
755
833
# NOTE the only 2 "True" return points:
869
947
 
870
948
@hooks.hook()
871
949
def config_changed(force_restart=False):
 
950
    validate_config()
872
951
    config_data = hookenv.config()
873
952
    update_repos_and_packages()
874
953
 
908
987
    postgresql_hba = os.path.join(postgresql_config_dir, "pg_hba.conf")
909
988
    postgresql_ident = os.path.join(postgresql_config_dir, "pg_ident.conf")
910
989
 
911
 
    current_service_port = get_service_port(postgresql_config)
912
990
    create_postgresql_config(postgresql_config)
 
991
    create_postgresql_ident(postgresql_ident)  # Do this before pg_hba.conf.
913
992
    generate_postgresql_hba(postgresql_hba)
914
 
    create_postgresql_ident(postgresql_ident)
915
993
    create_ssl_cert(os.path.join(
916
994
        postgresql_data_dir, pg_version(), config_data['cluster_name']))
917
 
 
918
 
    updated_service_port = config_data["listen_port"]
919
 
    update_service_port(current_service_port, updated_service_port)
 
995
    update_service_port()
920
996
    update_nrpe_checks()
921
997
    if force_restart:
922
 
        return postgresql_restart()
923
 
    return postgresql_reload_or_restart()
 
998
        postgresql_restart()
 
999
    postgresql_reload_or_restart()
924
1000
 
925
1001
 
926
1002
@hooks.hook()
930
1006
            if os.path.isfile(f) and os.access(f, os.X_OK):
931
1007
                subprocess.check_call(['sh', '-c', f])
932
1008
 
 
1009
    validate_config()
 
1010
 
933
1011
    config_data = hookenv.config()
934
1012
    update_repos_and_packages()
935
1013
    if not 'state' in local_state:
938
1016
        # any non-idempotent setup. We should probably fix this; it
939
1017
        # seems rather fragile.
940
1018
        local_state.setdefault('state', 'standalone')
941
 
        local_state.publish()
942
1019
 
943
1020
        # Drop the cluster created when the postgresql package was
944
1021
        # installed, and rebuild it with the requested locale and encoding.
945
1022
        version = pg_version()
946
 
        run("pg_dropcluster --stop {} main".format(version))
947
 
        run("pg_createcluster --locale='{}' --encoding='{}' {} main".format(
948
 
            config_data['locale'], config_data['encoding'], version))
 
1023
        for ver, name in lsclusters(slice(2)):
 
1024
            if version == ver and name == 'main':
 
1025
                run("pg_dropcluster --stop {} main".format(version))
 
1026
        listen_port = config_data.get('listen_port', None)
 
1027
        if listen_port:
 
1028
            port_opt = "--port={}".format(config_data['listen_port'])
 
1029
        else:
 
1030
            port_opt = ''
 
1031
        with switch_cwd('/tmp'):
 
1032
            create_cmd = [
 
1033
                "pg_createcluster",
 
1034
                "--locale", config_data['locale'],
 
1035
                "-e", config_data['encoding']]
 
1036
            if listen_port:
 
1037
                create_cmd.extend(["-p", str(config_data['listen_port'])])
 
1038
            create_cmd.append(pg_version())
 
1039
            create_cmd.append(config_data['cluster_name'])
 
1040
            run(create_cmd)
 
1041
        assert (
 
1042
            not port_opt
 
1043
            or get_service_port() == config_data['listen_port']), (
 
1044
            'allocated port {!r} != {!r}'.format(
 
1045
                get_service_port(), config_data['listen_port']))
 
1046
        local_state['port'] = get_service_port()
 
1047
        local_state.publish()
949
1048
 
950
1049
    postgresql_backups_dir = (
951
1050
        config_data['backup_dir'].strip() or
972
1071
        '{}/pg_backup_job'.format(postgresql_scripts_dir),
973
1072
        backup_job, perms=0755)
974
1073
    install_postgresql_crontab(postgresql_crontab)
975
 
    hookenv.open_port(5432)
 
1074
    hookenv.open_port(get_service_port())
976
1075
 
977
1076
    # Ensure at least minimal access granted for hooks to run.
978
1077
    # Reload because we are using the default cluster setup and started
990
1089
 
991
1090
@hooks.hook()
992
1091
def start():
993
 
    if not postgresql_reload_or_restart():
994
 
        raise SystemExit(1)
 
1092
    postgresql_reload_or_restart()
995
1093
 
996
1094
 
997
1095
@hooks.hook()
998
1096
def stop():
999
1097
    if postgresql_is_running():
1000
1098
        with restart_lock(hookenv.local_unit(), True):
1001
 
            if not postgresql_stop():
1002
 
                raise SystemExit(1)
 
1099
            postgresql_stop()
1003
1100
 
1004
1101
 
1005
1102
def quote_identifier(identifier):
1262
1359
    schema_password = create_user(schema_user)
1263
1360
    ensure_database(user, schema_user, database)
1264
1361
    host = hookenv.unit_private_ip()
1265
 
    port = hookenv.config('listen_port')
 
1362
    port = get_service_port()
1266
1363
    state = local_state['state']  # master, hot standby, standalone
1267
1364
 
1268
1365
    # Publish connection details.
1301
1398
 
1302
1399
    password = create_user(user, admin=True)
1303
1400
    host = hookenv.unit_private_ip()
1304
 
    port = hookenv.config('listen_port')
 
1401
    port = get_service_port()
1305
1402
    state = local_state['state']  # master, hot standby, standalone
1306
1403
 
1307
1404
    # Publish connection details.
1386
1483
 
1387
1484
 
1388
1485
def update_repos_and_packages():
1389
 
    extra_repos = hookenv.config('extra_archives')
1390
 
    extra_repos_added = local_state.setdefault('extra_repos_added', set())
1391
 
    if extra_repos:
1392
 
        repos_added = False
1393
 
        for repo in extra_repos.split():
1394
 
            if repo not in extra_repos_added:
1395
 
                fetch.add_source(repo)
1396
 
                extra_repos_added.add(repo)
1397
 
                repos_added = True
1398
 
        if repos_added:
1399
 
            fetch.apt_update(fatal=True)
1400
 
            local_state.save()
 
1486
    need_upgrade = False
 
1487
 
 
1488
    # Add the PGDG APT repository if it is enabled. Setting this boolean
 
1489
    # is simpler than requiring the magic URL and key be added to
 
1490
    # install_sources and install_keys. In addition, per Bug #1271148,
 
1491
    # install_keys is likely a security hole for this sort of remote
 
1492
    # archive. Instead, we keep a copy of the signing key in the charm
 
1493
    # and can add it securely.
 
1494
    pgdg_list = '/etc/apt/sources.list.d/pgdg_{}.list'.format(
 
1495
        sanitize(hookenv.local_unit()))
 
1496
    pgdg_key = 'ACCC4CF8'
 
1497
 
 
1498
    if hookenv.config('pgdg'):
 
1499
        if not os.path.exists(pgdg_list):
 
1500
            # We need to upgrade, as if we have Ubuntu main packages
 
1501
            # installed they may be incompatible with the PGDG ones.
 
1502
            # This is unlikely to ever happen outside of the test suite,
 
1503
            # and never if you don't reuse machines.
 
1504
            need_upgrade = True
 
1505
            run("apt-key add lib/{}.asc".format(pgdg_key))
 
1506
            open(pgdg_list, 'w').write('deb {} {}-pgdg main'.format(
 
1507
                'http://apt.postgresql.org/pub/repos/apt/', distro_codename()))
 
1508
    elif os.path.exists(pgdg_list):
 
1509
        log(
 
1510
            "PGDG apt source not requested, but already in place in this "
 
1511
            "container", WARNING)
 
1512
        # We can't just remove a source, as we may have packages
 
1513
        # installed that conflict with ones from the other configured
 
1514
        # sources. In particular, if we have postgresql-common installed
 
1515
        # from the PGDG Apt source, PostgreSQL packages from Ubuntu main
 
1516
        # will fail to install.
 
1517
        # os.unlink(pgdg_list)
 
1518
 
 
1519
    # Try to optimize our calls to fetch.configure_sources(), as it
 
1520
    # cannot do this itself due to lack of state.
 
1521
    if (need_upgrade
 
1522
        or local_state.get('install_sources', None)
 
1523
            != hookenv.config('install_sources')
 
1524
        or local_state.get('install_keys', None)
 
1525
            != hookenv.config('install_keys')):
 
1526
        # Support the standard mechanism implemented by charm-helpers. Pulls
 
1527
        # from the default 'install_sources' and 'install_keys' config
 
1528
        # options. This also does 'apt-get update', pulling in the PGDG data
 
1529
        # if we just configured it.
 
1530
        fetch.configure_sources(True)
 
1531
        local_state['install_sources'] = hookenv.config('install_sources')
 
1532
        local_state['install_keys'] = hookenv.config('install_keys')
 
1533
        local_state.save()
 
1534
 
 
1535
    # Ensure that the desired database locale is possible.
 
1536
    if hookenv.config('locale') != 'C':
 
1537
        run(["locale-gen", "{}.{}".format(
 
1538
            hookenv.config('locale'), hookenv.config('encoding'))])
 
1539
 
 
1540
    if need_upgrade:
 
1541
        run("apt-get -y upgrade")
1401
1542
 
1402
1543
    version = pg_version()
1403
1544
    # It might have been better for debversion and plpython to only get
1405
1546
    # but they predate this feature.
1406
1547
    packages = ["python-psutil",  # to obtain system RAM from python
1407
1548
                "libc-bin",       # for getconf
1408
 
                "postgresql-%s" % version,
1409
 
                "postgresql-contrib-%s" % version,
1410
 
                "postgresql-plpython-%s" % version,
1411
 
                "postgresql-%s-debversion" % version,
 
1549
                "postgresql-{}".format(version),
 
1550
                "postgresql-contrib-{}".format(version),
 
1551
                "postgresql-plpython-{}".format(version),
1412
1552
                "python-jinja2", "syslinux", "python-psycopg2"]
 
1553
    # PGDG currently doesn't have debversion for 9.3. Put this back when
 
1554
    # it does.
 
1555
    if not (hookenv.config('pgdg') and version == '9.3'):
 
1556
        "postgresql-{}-debversion".format(version)
1413
1557
    packages.extend((hookenv.config('extra-packages') or '').split())
1414
1558
    packages = fetch.filter_installed_packages(packages)
1415
1559
    fetch.apt_install(packages, fatal=True)
1472
1616
    '''Connect the database as a streaming replica of the master.'''
1473
1617
    master_relation = hookenv.relation_get(unit=master)
1474
1618
    create_recovery_conf(
1475
 
        master_relation['private-address'], restart_on_change=True)
 
1619
        master_relation['private-address'],
 
1620
        master_relation['port'], restart_on_change=True)
1476
1621
 
1477
1622
 
1478
1623
def elected_master():
1618
1763
            local_state['replication_password'] = replication_password
1619
1764
            local_state['client_relations'] = ' '.join(
1620
1765
                hookenv.relation_ids('db') + hookenv.relation_ids('db-admin'))
 
1766
            local_state.publish()
1621
1767
 
1622
1768
        else:
1623
1769
            log("I am master and remain master")
1635
1781
                master))
1636
1782
 
1637
1783
            master_ip = hookenv.relation_get('private-address', master)
 
1784
            master_port = hookenv.relation_get('port', master)
 
1785
            assert master_port is not None, 'No master port set'
1638
1786
 
1639
 
            clone_database(master, master_ip)
 
1787
            clone_database(master, master_ip, master_port)
1640
1788
 
1641
1789
            local_state['state'] = 'hot standby'
1642
1790
            local_state['following'] = master
1711
1859
 
1712
1860
        # Override unit specific connection details
1713
1861
        connection_settings['host'] = hookenv.unit_private_ip()
1714
 
        connection_settings['port'] = hookenv.config('listen_port')
 
1862
        connection_settings['port'] = get_service_port()
1715
1863
        connection_settings['state'] = local_state['state']
1716
1864
 
1717
1865
        # Block until users and database has replicated, so we know the
1833
1981
                cur = db_cursor(autocommit=True)
1834
1982
            else:
1835
1983
                host = hookenv.relation_get('private-address', unit)
 
1984
                port = hookenv.relation_get('port', unit)
1836
1985
                cur = db_cursor(
1837
 
                    autocommit=True, db='postgres',
1838
 
                    user='juju_replication', host=host)
 
1986
                    autocommit=True, db='postgres', user='juju_replication',
 
1987
                    host=host, port=port)
1839
1988
            cur.execute(q)
1840
1989
            break
1841
1990
        except psycopg2.Error:
1853
2002
            pass
1854
2003
 
1855
2004
 
1856
 
def clone_database(master_unit, master_host):
 
2005
def clone_database(master_unit, master_host, master_port):
1857
2006
    with restart_lock(master_unit, False):
1858
2007
        postgresql_stop()
1859
2008
        log("Cloning master {}".format(master_unit))
1868
2017
            'sudo', '-E',  # -E needed to locate pgpass file.
1869
2018
            '-u', 'postgres', 'pg_basebackup', '-D', postgresql_cluster_dir,
1870
2019
            '--xlog', '--checkpoint=fast', '--no-password',
1871
 
            '-h', master_host, '-p', '5432', '--username=juju_replication']
 
2020
            '-h', master_host, '-p', master_port,
 
2021
            '--username=juju_replication']
1872
2022
        log(' '.join(cmd), DEBUG)
1873
2023
 
1874
2024
        if os.path.isdir(postgresql_cluster_dir):
1883
2033
            log(output, DEBUG)
1884
2034
            # Debian by default expects SSL certificates in the datadir.
1885
2035
            create_ssl_cert(postgresql_cluster_dir)
1886
 
            create_recovery_conf(master_host)
 
2036
            create_recovery_conf(master_host, master_port)
1887
2037
        except subprocess.CalledProcessError as x:
1888
2038
            # We failed, and this cluster is broken. Rebuild a
1889
2039
            # working cluster so start/stop etc. works and we
1955
2105
    return int(logid, 16) * 16 * 1024 * 1024 * 255 + int(offset, 16)
1956
2106
 
1957
2107
 
1958
 
def wait_for_db(timeout=120, db='postgres', user='postgres', host=None):
 
2108
def wait_for_db(
 
2109
        timeout=120, db='postgres', user='postgres', host=None, port=None):
1959
2110
    '''Wait until the db is fully up.'''
1960
 
    db_cursor(db=db, user=user, host=host, timeout=timeout)
 
2111
    db_cursor(db=db, user=user, host=host, port=port, timeout=timeout)
1961
2112
 
1962
2113
 
1963
2114
def unit_sorted(units):
2016
2167
        nrpe_check_config.write("# check pgsql\n")
2017
2168
        nrpe_check_config.write(
2018
2169
            "command[check_pgsql]=/usr/lib/nagios/plugins/check_pgsql -P {}"
2019
 
            .format(config_data['listen_port']))
 
2170
            .format(get_service_port()))
2020
2171
    # pgsql backups
2021
2172
    nrpe_check_file = '/etc/nagios/nrpe.d/check_pgsql_backups.cfg'
2022
2173
    backup_log = "{}/backups.log".format(postgresql_logs_dir)