60
60
package candidate, saving it in local_state for later.
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']
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',
73
version = version_map.get(distro_release)
71
version = version_map.get(distro_codename())
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()),
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)
242
246
startup_file, contents, 'postgres', 'postgres', perms=0o644)
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.'''
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)
254
sys.exit(e.returncode)
251
log("Running {!r}".format(command), DEBUG)
252
p = subprocess.Popen(
253
command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
254
shell=isinstance(command, basestring))
257
for line in p.stdout:
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)
266
elif p.poll() is not None:
271
if p.returncode == 0:
272
return '\n'.join(lines)
274
if p.returncode != 0 and exit_on_error:
275
log("ERROR: {}".format(p.returncode), ERROR)
276
sys.exit(p.returncode)
278
raise subprocess.CalledProcessError(
279
p.returncode, command, '\n'.join(lines))
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")
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)
290
log('PostgreSQL is not running', DEBUG)
292
assert False, 'Cluster {} {} not found'.format(
293
pg_version(), hookenv.config('cluster_name'))
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():
300
'pg_ctlcluster', '--force',
301
pg_version(), hookenv.config('cluster_name'), 'stop'])
302
log('PostgreSQL shut down')
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():
309
'pg_ctlcluster', pg_version(),
310
hookenv.config('cluster_name'), 'start'])
311
log('PostgreSQL started')
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')
289
run('pg_ctlcluster -force {} {} '
290
'restart'.format(pg_version(),
291
hookenv.config('cluster_name')))
293
except subprocess.CalledProcessError:
319
'pg_ctlcluster', '--force',
320
pg_version(), hookenv.config('cluster_name'), 'restart'])
321
log('PostgreSQL restarted')
296
success = host.service_start('postgresql')
325
assert postgresql_is_running()
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()
304
return success and postgresql_is_running()
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")
337
if postgresql_is_running():
338
# I'm using the PostgreSQL function to avoid as much indirection
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()
314
346
def requires_restart():
340
372
# A setting has changed that requires PostgreSQL to be
341
373
# restarted before it will take effect.
375
log('{} changed from {} to {}. Restart required.'.format(
376
name, live_value, new_value), DEBUG)
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.",
351
success = postgresql_restart()
352
if not success or requires_restart():
353
log("Configuration changes failed to apply", WARNING)
383
log("Configuration change requires PostgreSQL restart", WARNING)
385
assert not requires_restart(), "Configuration changes failed to apply"
356
success = host.service_reload('postgresql')
359
local_state['saved_config'] = local_state['live_config']
365
def get_service_port(config_file):
389
local_state['saved_config'] = local_state['live_config']
393
def get_service_port():
366
394
'''Return the port PostgreSQL is listening on.'''
367
if not os.path.exists(config_file):
369
postgresql_config = open(config_file, 'r').read()
370
port = re.search("port.*=(.*)", postgresql_config).group(1).strip()
373
except (ValueError, TypeError):
395
for version, name, port in lsclusters(slice(3)):
396
if (version, name) == (pg_version(), hookenv.config('cluster_name')):
399
assert False, 'No port found for {!r} {!r}'.format(
400
pg_version(), hookenv.config['cluster_name'])
403
def lsclusters(s=slice(0, -1)):
404
for line in run('pg_lsclusters', quiet=True).splitlines()[1:]:
406
yield line.split()[s]
377
409
def _get_system_ram():
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:
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:
700
hookenv.open_port(new_port)
702
hookenv.close_port(old_port)
703
local_state['listen_port'] = new_port
673
707
def create_ssl_cert(cluster_dir):
750
787
return (cur.rowcount, results)
790
def validate_config():
792
Sanity check charm configuration, aborting the script if
793
we have bogus config values or config changes the charm does not yet
797
config_data = hookenv.config()
799
version = config_data.get('version', None)
801
if version not in ('9.1', '9.2', '9.3'):
803
log("Invalid or unsupported version {!r} requested".format(
806
if config_data['cluster_name'] != 'main':
808
log("Cluster names other than 'main' do not work per LP:1271835",
811
if config_data['listen_ip'] != '*':
813
log("listen_ip values other than '*' do not work per LP:1271837",
816
unchangeable_config = [
817
'locale', 'encoding', 'version', 'cluster_name', 'pgdg']
819
for name in unchangeable_config:
820
if (name in local_state
821
and local_state[name] != config_data.get(name, None)):
823
log("Cannot change {!r} setting after install.".format(name))
824
local_state[name] = config_data.get(name, None)
753
831
#------------------------------------------------------------------------------
754
832
# Core logic for permanent storage changes:
755
833
# NOTE the only 2 "True" return points:
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()
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)
1028
port_opt = "--port={}".format(config_data['listen_port'])
1031
with switch_cwd('/tmp'):
1034
"--locale", config_data['locale'],
1035
"-e", config_data['encoding']]
1037
create_cmd.extend(["-p", str(config_data['listen_port'])])
1038
create_cmd.append(pg_version())
1039
create_cmd.append(config_data['cluster_name'])
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()
950
1049
postgresql_backups_dir = (
951
1050
config_data['backup_dir'].strip() or
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())
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)
1399
fetch.apt_update(fatal=True)
1486
need_upgrade = False
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'
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.
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):
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)
1519
# Try to optimize our calls to fetch.configure_sources(), as it
1520
# cannot do this itself due to lack of state.
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')
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'))])
1541
run("apt-get -y upgrade")
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
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)