Merge lp:~stub/charms/precise/postgresql/bug-1205286 into lp:charms/postgresql

Proposed by Stuart Bishop
Status: Merged
Approved by: Mark Mims
Approved revision: 66
Merged at revision: 61
Proposed branch: lp:~stub/charms/precise/postgresql/bug-1205286
Merge into: lp:charms/postgresql
Prerequisite: lp:~stub/charms/precise/postgresql/charm-helpers
Diff against target: 295 lines (+81/-79)
2 files modified
hooks/hooks.py (+69/-75)
test.py (+12/-4)
To merge this branch: bzr merge lp:~stub/charms/precise/postgresql/bug-1205286
Reviewer Review Type Date Requested Status
Mark Mims (community) Approve
Review via email: mp+181063@code.launchpad.net

Description of the change

Instead of storing pgpass files in the charm directory where sudo subprocesses can no longer access them, generate them as required to a temporary file.

A few minor drivebys, such as using pwgen from charmhelpers.

To post a comment you must log in.
Revision history for this message
Mark Mims (mark-mims) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2013-08-09 19:33:47 +0000
+++ hooks/hooks.py 2013-08-20 14:59:39 +0000
@@ -1,19 +1,19 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# vim: et ai ts=4 sw=4:2# vim: et ai ts=4 sw=4:
33
4from contextlib import contextmanager
4import commands5import commands
5import cPickle as pickle6import cPickle as pickle
6import glob7import glob
7from grp import getgrnam8from grp import getgrnam
8import os.path9import os.path
9from pwd import getpwnam10from pwd import getpwnam
10import random
11import re11import re
12import shutil12import shutil
13import socket13import socket
14import string
15import subprocess14import subprocess
16import sys15import sys
16from tempfile import NamedTemporaryFile
17import time17import time
18import yaml18import yaml
19from yaml.constructor import ConstructorError19from yaml.constructor import ConstructorError
@@ -545,7 +545,7 @@
545 write_file('/etc/cron.d/postgres', crontab_template, perms=0600)545 write_file('/etc/cron.d/postgres', crontab_template, perms=0600)
546546
547547
548def create_recovery_conf(master_host, password, restart_on_change=False):548def create_recovery_conf(master_host, restart_on_change=False):
549 recovery_conf_path = os.path.join(postgresql_cluster_dir, 'recovery.conf')549 recovery_conf_path = os.path.join(postgresql_cluster_dir, 'recovery.conf')
550 if os.path.exists(recovery_conf_path):550 if os.path.exists(recovery_conf_path):
551 old_recovery_conf = open(recovery_conf_path, 'r').read()551 old_recovery_conf = open(recovery_conf_path, 'r').read()
@@ -592,17 +592,6 @@
592 hookenv.open_port(new_service_port)592 hookenv.open_port(new_service_port)
593593
594594
595def pwgen(pwd_length=None):
596 '''Generate a random password.'''
597 if pwd_length is None:
598 pwd_length = random.choice(range(30, 40))
599 alphanumeric_chars = [l for l in (string.letters + string.digits)
600 if l not in 'Iil0oO1']
601 random_chars = [random.choice(alphanumeric_chars)
602 for i in range(pwd_length)]
603 return(''.join(random_chars))
604
605
606def set_password(user, password):595def set_password(user, password):
607 if not os.path.isdir("passwords"):596 if not os.path.isdir("passwords"):
608 os.makedirs("passwords")597 os.makedirs("passwords")
@@ -638,7 +627,8 @@
638 start = time.time()627 start = time.time()
639 while True:628 while True:
640 try:629 try:
641 conn = psycopg2.connect(conn_str)630 with pgpass():
631 conn = psycopg2.connect(conn_str)
642 break632 break
643 except psycopg2.Error, x:633 except psycopg2.Error, x:
644 if time.time() > start + timeout:634 if time.time() > start + timeout:
@@ -825,7 +815,6 @@
825 updated_service_port = config_data["listen_port"]815 updated_service_port = config_data["listen_port"]
826 update_service_port(current_service_port, updated_service_port)816 update_service_port(current_service_port, updated_service_port)
827 update_nrpe_checks()817 update_nrpe_checks()
828 generate_pgpass()
829 if force_restart:818 if force_restart:
830 return postgresql_restart()819 return postgresql_restart()
831 return postgresql_reload_or_restart()820 return postgresql_reload_or_restart()
@@ -840,7 +829,7 @@
840829
841 add_extra_repos()830 add_extra_repos()
842831
843 packages = ["postgresql", "pwgen", "python-jinja2", "syslinux",832 packages = ["postgresql", "python-jinja2", "syslinux",
844 "python-psycopg2", "postgresql-contrib", "postgresql-plpython",833 "python-psycopg2", "postgresql-contrib", "postgresql-plpython",
845 "postgresql-%s-debversion" % config_data["version"]]834 "postgresql-%s-debversion" % config_data["version"]]
846 packages.extend((hookenv.config('extra-packages') or '').split())835 packages.extend((hookenv.config('extra-packages') or '').split())
@@ -984,7 +973,7 @@
984973
985 password = get_password(user)974 password = get_password(user)
986 if password is None:975 if password is None:
987 password = pwgen()976 password = host.pwgen()
988 set_password(user, password)977 set_password(user, password)
989 if user_exists(user):978 if user_exists(user):
990 log("Updating {} user".format(user))979 log("Updating {} user".format(user))
@@ -1317,7 +1306,8 @@
1317 local_state.publish()1306 local_state.publish()
13181307
13191308
1320def generate_pgpass():1309@contextmanager
1310def pgpass():
1321 passwords = {}1311 passwords = {}
13221312
1323 # Replication.1313 # Replication.
@@ -1326,13 +1316,23 @@
1326 if 'replication_password' in local_state:1316 if 'replication_password' in local_state:
1327 passwords['juju_replication'] = local_state['replication_password']1317 passwords['juju_replication'] = local_state['replication_password']
13281318
1329 if passwords:1319 pgpass_contents = '\n'.join(
1330 pgpass = '\n'.join(1320 "*:*:*:{}:{}".format(username, password)
1331 "*:*:*:{}:{}".format(username, password)1321 for username, password in passwords.items())
1332 for username, password in passwords.items())1322 pgpass_file = NamedTemporaryFile()
1333 write_file(1323 pgpass_file.write(pgpass_contents)
1334 charm_pgpass, pgpass,1324 pgpass_file.flush()
1335 owner="postgres", group="postgres", perms=0o400)1325 os.chown(pgpass_file.name, getpwnam('postgres').pw_uid, -1)
1326 os.chmod(pgpass_file.name, 0o400)
1327 org_pgpassfile = os.environ.get('PGPASSFILE', None)
1328 os.environ['PGPASSFILE'] = pgpass_file.name
1329 try:
1330 yield pgpass_file.name
1331 finally:
1332 if org_pgpassfile is None:
1333 del os.environ['PGPASSFILE']
1334 else:
1335 os.environ['PGPASSFILE'] = org_pgpassfile
13361336
13371337
1338def drop_database(dbname, warn=True):1338def drop_database(dbname, warn=True):
@@ -1377,8 +1377,7 @@
1377 '''Connect the database as a streaming replica of the master.'''1377 '''Connect the database as a streaming replica of the master.'''
1378 master_relation = hookenv.relation_get(unit=master)1378 master_relation = hookenv.relation_get(unit=master)
1379 create_recovery_conf(1379 create_recovery_conf(
1380 master_relation['private-address'],1380 master_relation['private-address'], restart_on_change=True)
1381 local_state['replication_password'], restart_on_change=True)
13821381
13831382
1384def elected_master():1383def elected_master():
@@ -1498,7 +1497,6 @@
1498 log("Syncing replication_password from {}".format(master), DEBUG)1497 log("Syncing replication_password from {}".format(master), DEBUG)
1499 local_state['replication_password'] = hookenv.relation_get(1498 local_state['replication_password'] = hookenv.relation_get(
1500 'replication_password', master)1499 'replication_password', master)
1501 generate_pgpass()
15021500
1503 if 'following' not in local_state:1501 if 'following' not in local_state:
1504 log("Fresh unit. I will clone {} and become a hot standby".format(1502 log("Fresh unit. I will clone {} and become a hot standby".format(
@@ -1526,7 +1524,7 @@
1526 else:1524 else:
1527 log("I am a hot standby following new master {}".format(master))1525 log("I am a hot standby following new master {}".format(master))
1528 follow_database(master)1526 follow_database(master)
1529 if not local_state["paused_at_failover"]:1527 if not local_state.get("paused_at_failover", None):
1530 run_sql_as_postgres("SELECT pg_xlog_replay_resume()")1528 run_sql_as_postgres("SELECT pg_xlog_replay_resume()")
1531 local_state['state'] = 'hot standby'1529 local_state['state'] = 'hot standby'
1532 local_state['following'] = master1530 local_state['following'] = master
@@ -1664,51 +1662,52 @@
1664def replication_relation_broken():1662def replication_relation_broken():
1665 # This unit has been removed from the service.1663 # This unit has been removed from the service.
1666 promote_database()1664 promote_database()
1667 if os.path.exists(charm_pgpass):
1668 os.unlink(charm_pgpass)
1669 config_changed()1665 config_changed()
16701666
16711667
1672def clone_database(master_unit, master_host):1668def clone_database(master_unit, master_host):
1673 postgresql_stop()1669 with pgpass():
1674 log("Cloning master {}".format(master_unit))1670 postgresql_stop()
16751671 log("Cloning master {}".format(master_unit))
1676 cmd = ['sudo', '-E', '-u', 'postgres', # -E needed to locate pgpass file.1672
1677 'pg_basebackup', '-D', postgresql_cluster_dir,1673 cmd = ['sudo', '-E', # -E needed to locate pgpass file.
1678 '--xlog', '--checkpoint=fast', '--no-password',1674 '-u', 'postgres', 'pg_basebackup', '-D', postgresql_cluster_dir,
1679 '-h', master_host, '-p', '5432', '--username=juju_replication']1675 '--xlog', '--checkpoint=fast', '--no-password',
1680 log(' '.join(cmd), DEBUG)1676 '-h', master_host, '-p', '5432', '--username=juju_replication']
1681 if os.path.isdir(postgresql_cluster_dir):1677 log(' '.join(cmd), DEBUG)
1682 shutil.rmtree(postgresql_cluster_dir)1678
1683 try:1679 if os.path.isdir(postgresql_cluster_dir):
1684 output = subprocess.check_output(cmd)
1685 log(output, DEBUG)
1686 # Debian by default expects SSL certificates in the datadir.
1687 os.symlink(
1688 '/etc/ssl/certs/ssl-cert-snakeoil.pem',
1689 os.path.join(postgresql_cluster_dir, 'server.crt'))
1690 os.symlink(
1691 '/etc/ssl/private/ssl-cert-snakeoil.key',
1692 os.path.join(postgresql_cluster_dir, 'server.key'))
1693 create_recovery_conf(master_host, local_state['replication_password'])
1694 except subprocess.CalledProcessError, x:
1695 # We failed, and this cluster is broken. Rebuild a
1696 # working cluster so start/stop etc. works and we
1697 # can retry hooks again. Even assuming the charm is
1698 # functioning correctly, the clone may still fail
1699 # due to eg. lack of disk space.
1700 log("Clone failed, db cluster destroyed", ERROR)
1701 log(x.output, ERROR)
1702 if os.path.exists(postgresql_cluster_dir):
1703 shutil.rmtree(postgresql_cluster_dir)1680 shutil.rmtree(postgresql_cluster_dir)
1704 if os.path.exists(postgresql_config_dir):1681
1705 shutil.rmtree(postgresql_config_dir)1682 try:
1706 run('pg_createcluster {} main'.format(version))1683 output = subprocess.check_output(cmd)
1707 config_changed()1684 log(output, DEBUG)
1708 raise1685 # Debian by default expects SSL certificates in the datadir.
1709 finally:1686 os.symlink(
1710 postgresql_start()1687 '/etc/ssl/certs/ssl-cert-snakeoil.pem',
1711 wait_for_db()1688 os.path.join(postgresql_cluster_dir, 'server.crt'))
1689 os.symlink(
1690 '/etc/ssl/private/ssl-cert-snakeoil.key',
1691 os.path.join(postgresql_cluster_dir, 'server.key'))
1692 create_recovery_conf(master_host)
1693 except subprocess.CalledProcessError, x:
1694 # We failed, and this cluster is broken. Rebuild a
1695 # working cluster so start/stop etc. works and we
1696 # can retry hooks again. Even assuming the charm is
1697 # functioning correctly, the clone may still fail
1698 # due to eg. lack of disk space.
1699 log("Clone failed, db cluster destroyed", ERROR)
1700 log(x.output, ERROR)
1701 if os.path.exists(postgresql_cluster_dir):
1702 shutil.rmtree(postgresql_cluster_dir)
1703 if os.path.exists(postgresql_config_dir):
1704 shutil.rmtree(postgresql_config_dir)
1705 run('pg_createcluster {} main'.format(version))
1706 config_changed()
1707 raise
1708 finally:
1709 postgresql_start()
1710 wait_for_db()
17121711
17131712
1714def slave_count():1713def slave_count():
@@ -1851,11 +1850,6 @@
1851hook_name = os.path.basename(sys.argv[0])1850hook_name = os.path.basename(sys.argv[0])
1852replication_relation_types = ['master', 'slave', 'replication']1851replication_relation_types = ['master', 'slave', 'replication']
1853local_state = State('local_state.pickle')1852local_state = State('local_state.pickle')
1854charm_pgpass = os.path.abspath(
1855 os.path.join(os.path.dirname(__file__), '..', 'pgpass'))
1856
1857# Hooks, running as root, need to be pointed at the correct .pgpass.
1858os.environ['PGPASSFILE'] = charm_pgpass
18591853
18601854
1861if __name__ == '__main__':1855if __name__ == '__main__':
18621856
=== modified file 'test.py'
--- test.py 2013-07-08 11:04:27 +0000
+++ test.py 2013-08-20 14:59:39 +0000
@@ -21,7 +21,7 @@
2121
22SERIES = 'precise'22SERIES = 'precise'
23TEST_CHARM = 'local:postgresql'23TEST_CHARM = 'local:postgresql'
24PSQL_CHARM = 'cs:postgresql-psql'24PSQL_CHARM = 'local:postgresql-psql'
2525
2626
27def DEBUG(msg):27def DEBUG(msg):
@@ -118,9 +118,17 @@
118 unit, units[unit].get('agent-state-info','')))118 unit, units[unit].get('agent-state-info','')))
119 if agent_state != 'started':119 if agent_state != 'started':
120 ready = False120 ready = False
121 # Wait a little longer, as we have no way of telling121 # Unfortunately, there is no way to tell when a system is
122 # if relationship hooks have finished running.122 # actually ready for us to test. Juju only tells us that a
123 time.sleep(10)123 # relation has started being setup, and that no errors have been
124 # encountered yet. It utterly fails to inform us when the
125 # cascade of hooks this triggers has finished and the
126 # environment is in a stable and actually testable state.
127 # So as a work around for Bug #1200267, we need to sleep long
128 # enough that our system is probably stable. This means we have
129 # extremely slow and flaky tests, but that is possibly better
130 # than no tests.
131 time.sleep(30)
124132
125 def setUp(self):133 def setUp(self):
126 DEBUG("JujuFixture.setUp()")134 DEBUG("JujuFixture.setUp()")

Subscribers

People subscribed via source and target branches