Merge lp:~stub/charms/precise/postgresql/pg93 into lp:charms/postgresql
- Precise Pangolin (12.04)
- pg93
- Merge into trunk
Status: | Merged | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 86 | ||||||||||||
Proposed branch: | lp:~stub/charms/precise/postgresql/pg93 | ||||||||||||
Merge into: | lp:charms/postgresql | ||||||||||||
Prerequisite: | lp:~stub/charms/precise/postgresql/tests | ||||||||||||
Diff against target: |
1315 lines (+538/-184) 19 files modified
.bzrignore (+3/-0) Makefile (+20/-3) README.md (+6/-3) config.yaml (+38/-15) hooks/charmhelpers/fetch/__init__.py (+2/-2) hooks/hooks.py (+283/-132) lib/ACCC4CF8.asc (+45/-0) templates/pg_hba.conf.tmpl (+1/-1) templates/pg_ident.conf.tmpl (+1/-0) templates/postgresql.conf.tmpl (+5/-0) templates/recovery.conf.tmpl (+1/-1) test.py (+91/-21) testing/jujufixture.py (+17/-5) tests/00_setup.test (+12/-0) tests/01_lint.test (+3/-0) tests/02_unit_test.test (+1/-1) tests/91_integration_test_91.test (+3/-0) tests/92_integration_test_92.test (+3/-0) tests/93_integration_test_93.test (+3/-0) |
||||||||||||
To merge this branch: | bzr merge lp:~stub/charms/precise/postgresql/pg93 | ||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi (community) | Approve | ||
Review via email: mp+202841@code.launchpad.net |
Commit message
Description of the change
Run tests for all supported versions of PostgreSQL.
This involved several drive-bys, including:
- Allowing specifying a non-standard port to actually work.
- Realizing that the mechanism for specifying external sources was broken,
and replacing it with the charm-helpers tool. I was using a space separated
list, but unless you are using a PPA your source probably contains a space.
- Realizing that the current mechanism for charm-helpers to retrieve keys
is insecure, so implement a shortcut to add the official PG backports
securely that also makes it easier to use.
- Using null instead of empty string to indicate default/magic, probably
for personal and unjustifiable aethetic reasons. Empty string still works.
- Config validation, to catch dangerously bad config changes before code
that doesn't expect them destroys data. We cannot support version
downgrades, and we don't yet support version upgrades.
- 148. By Stuart Bishop
-
9.2 tests need PGDG archive or packages might not be found
- 149. By Stuart Bishop
-
Replication fixes
- 150. By Stuart Bishop
-
Fix admin test to work on non-local providers
- 151. By Stuart Bishop
-
charm-helpers for lsb_release
- 152. By Stuart Bishop
-
distro_release -> distro_codename
- 153. By Stuart Bishop
-
Merged tests into pg93.
- 154. By Stuart Bishop
-
Refactoring startup, shutdown & reload to better avoid config failures and race conditions like lp:1273636, and some fixes
- 155. By Stuart Bishop
-
Fixes
- 156. By Stuart Bishop
-
Fix PG 9.2 & 9.3 replication
- 157. By Stuart Bishop
-
Improve config validation
- 158. By Stuart Bishop
-
Make basic replication test more robust
- 159. By Stuart Bishop
-
Bump timeout to decrease flakeyness due to LP:1200267
- 160. By Stuart Bishop
-
Resolve conflicts
- 161. By Stuart Bishop
-
Merged tests into pg93.
- 162. By Stuart Bishop
-
Merged tests into pg93.
Marco Ceppi (marcoceppi) wrote : | # |
- 163. By Stuart Bishop
-
Better fit with juju test
- 164. By Stuart Bishop
-
delint
Stuart Bishop (stub) wrote : | # |
Thanks. I've delinted, and will land it once the dependent MP https:/
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2012-10-30 16:41:12 +0000 |
3 | +++ .bzrignore 2014-02-07 12:21:04 +0000 |
4 | @@ -0,0 +1,3 @@ |
5 | +_trial_temp |
6 | +hooks/_trial_temp |
7 | +hooks/local_state.pickle |
8 | |
9 | === modified file 'Makefile' |
10 | --- Makefile 2014-01-09 09:08:11 +0000 |
11 | +++ Makefile 2014-02-07 12:21:04 +0000 |
12 | @@ -1,13 +1,30 @@ |
13 | CHARM_DIR := $(shell pwd) |
14 | +TEST_TIMEOUT := 900 |
15 | |
16 | test: lint unit_test integration_test |
17 | |
18 | unit_test: |
19 | + @echo "Unit tests of hooks" |
20 | cd hooks && trial test_hooks.py |
21 | |
22 | integration_test: |
23 | - echo "Integration tests using Juju deployed units" |
24 | - TEST_TIMEOUT=900 ./test.py -v |
25 | + @echo "PostgreSQL integration tests, all versions" |
26 | + trial test |
27 | + |
28 | +integration_test_91: |
29 | + @echo "PostgreSQL 9.1 integration tests" |
30 | + trial test.PG91Tests |
31 | + |
32 | +integration_test_92: |
33 | + @echo "PostgreSQL 9.2 integration tests" |
34 | + trial test.PG92Tests |
35 | + |
36 | +integration_test_93: |
37 | + @echo "PostgreSQL 9.3 integration tests" |
38 | + trial test.PG93Tests |
39 | |
40 | lint: |
41 | - @flake8 --exclude hooks/charmhelpers hooks # requires python-flakes8 |
42 | + @echo "Lint check (flake8)" |
43 | + @flake8 -v \ |
44 | + --exclude hooks/charmhelpers,hooks/_trial_temp \ |
45 | + hooks testing tests test.py |
46 | |
47 | === modified file 'README.md' |
48 | --- README.md 2014-02-07 12:21:03 +0000 |
49 | +++ README.md 2014-02-07 12:21:04 +0000 |
50 | @@ -62,9 +62,12 @@ |
51 | To setup a client using a PostgreSQL database, in this case a vanilla Django |
52 | installation listening on port 8080:: |
53 | |
54 | - juju deploy postgresql juju deploy python-django juju deploy gunicorn juju |
55 | -add-relation python-django postgresql:db juju add-relation python-django |
56 | -gunicorn juju expose python-django |
57 | + juju deploy postgresql |
58 | + juju deploy python-django |
59 | + juju deploy gunicorn |
60 | + juju add-relation python-django postgresql:db |
61 | + juju add-relation python-django gunicorn |
62 | + juju expose python-django |
63 | |
64 | |
65 | ## Known Limitations and Issues |
66 | |
67 | === modified file 'config.yaml' |
68 | --- config.yaml 2014-01-22 07:09:38 +0000 |
69 | +++ config.yaml 2014-02-07 12:21:04 +0000 |
70 | @@ -35,17 +35,20 @@ |
71 | default: "reload" |
72 | type: string |
73 | description: | |
74 | - The command to run whenever config has changed. Accepted values are |
75 | - "reload" or "restart" - any other value will mean neither is executed |
76 | - after a config change (which may be desired, if you're running a |
77 | - production server and would rather handle these out of band). Note that |
78 | - postgresql will still need to be reloaded whenever authentication and |
79 | - access details are updated, so disabling either doesn't mean PostgreSQL |
80 | - will never be reloaded. |
81 | + The command to run whenever config has changed. Accepted values |
82 | + are "reload" or "restart" - any other value will mean neither is |
83 | + executed after a config change (which may be desired, if you're |
84 | + running a production server and would rather handle these out of |
85 | + band). Note that postgresql will still need to be reloaded |
86 | + whenever authentication and access details are updated, so |
87 | + disabling either doesn't mean PostgreSQL will never be reloaded. |
88 | version: |
89 | - default: "" |
90 | + default: null |
91 | type: string |
92 | - description: Version of PostgreSQL that we want to install |
93 | + description: | |
94 | + Version of PostgreSQL that we want to install. Supported versions |
95 | + are "9.1", "9.2", "9.3". The default version for the deployed Ubuntu |
96 | + release is used when the version is not specified. |
97 | cluster_name: |
98 | default: "main" |
99 | type: string |
100 | @@ -55,9 +58,9 @@ |
101 | type: string |
102 | description: IP to listen on |
103 | listen_port: |
104 | - default: 5432 |
105 | + default: null |
106 | type: int |
107 | - description: Port to listen on |
108 | + description: Port to listen on. Default is automatically assigned. |
109 | max_connections: |
110 | default: 100 |
111 | type: int |
112 | @@ -120,7 +123,9 @@ |
113 | autovacuum: |
114 | default: True |
115 | type: boolean |
116 | - description: Autovacuum should almost always be running. |
117 | + description: | |
118 | + Autovacuum should almost always be running. If you want to turn this |
119 | + off, you are probably following out of date documentation. |
120 | log_autovacuum_min_duration: |
121 | default: -1 |
122 | type: int |
123 | @@ -329,13 +334,31 @@ |
124 | juju-postgresql-0 |
125 | If you're running multiple environments with the same services in them |
126 | this allows you to differentiate between them. |
127 | + pgdg: |
128 | + description: | |
129 | + Enable the PostgreSQL Global Development Group APT repository |
130 | + (https://wiki.postgresql.org/wiki/Apt). This package source provides |
131 | + official PostgreSQL packages for Ubuntu LTS releases beyond those |
132 | + provided by the main Ubuntu archive. |
133 | + type: boolean |
134 | + default: false |
135 | + install_sources: |
136 | + description: | |
137 | + List of extra package sources, per charm-helpers standard. |
138 | + YAML format. |
139 | + type: string |
140 | + default: "" |
141 | + install_keys: |
142 | + description: | |
143 | + List of signing keys for install_sources package sources, per |
144 | + charmhelpers standard. YAML format. |
145 | + type: string |
146 | + default: "" |
147 | extra_archives: |
148 | default: "" |
149 | type: string |
150 | description: | |
151 | - Extra archives to add, space separated. Supports ppa:, http:, cloud: |
152 | - URIs, as well as other schemes and keywords supported by |
153 | - charmhelpers.fetch.add_source() such as "proposed". |
154 | + DEPRECATED & IGNORED. Use install_sources and install_keys. |
155 | advisory_lock_restart_key: |
156 | default: 765 |
157 | type: int |
158 | |
159 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
160 | --- hooks/charmhelpers/fetch/__init__.py 2013-09-24 11:09:35 +0000 |
161 | +++ hooks/charmhelpers/fetch/__init__.py 2014-02-07 12:21:04 +0000 |
162 | @@ -117,8 +117,8 @@ |
163 | |
164 | Note that 'null' (a.k.a. None) should not be quoted. |
165 | """ |
166 | - sources = safe_load(config(sources_var)) |
167 | - keys = safe_load(config(keys_var)) |
168 | + sources = safe_load(config(sources_var)) or [] |
169 | + keys = safe_load(config(keys_var)) or [] |
170 | if isinstance(sources, basestring) and isinstance(keys, basestring): |
171 | add_source(sources, keys) |
172 | else: |
173 | |
174 | === modified file 'hooks/hooks.py' |
175 | --- hooks/hooks.py 2014-02-07 12:21:03 +0000 |
176 | +++ hooks/hooks.py 2014-02-07 12:21:04 +0000 |
177 | @@ -60,24 +60,22 @@ |
178 | package candidate, saving it in local_state for later. |
179 | ''' |
180 | config_data = hookenv.config() |
181 | - if config_data['version']: |
182 | + if 'pg_version' in local_state: |
183 | + version = local_state['pg_version'] |
184 | + elif 'version' in config_data: |
185 | version = config_data['version'] |
186 | - elif 'pg_version' in local_state: |
187 | - version = local_state['pg_version'] |
188 | else: |
189 | log("map version from distro release ...") |
190 | - distro_release = run("lsb_release -sc") |
191 | - distro_release = distro_release.rstrip() |
192 | version_map = {'precise': '9.1', |
193 | 'trusty': '9.3'} |
194 | - version = version_map.get(distro_release) |
195 | + version = version_map.get(distro_codename()) |
196 | if not version: |
197 | - log("No PG version map for distro_release={}, " |
198 | - "you'll need to explicitly set it".format(distro_release), |
199 | + log("No PG version map for distro_codename={}, " |
200 | + "you'll need to explicitly set it".format(distro_codename()), |
201 | CRITICAL) |
202 | sys.exit(1) |
203 | - log("version={} from distro_release='{}'".format( |
204 | - version, distro_release)) |
205 | + log("version={} from distro_codename='{}'".format( |
206 | + version, distro_codename())) |
207 | # save it for later |
208 | local_state.setdefault('pg_version', version) |
209 | local_state.save() |
210 | @@ -86,6 +84,11 @@ |
211 | return version |
212 | |
213 | |
214 | +def distro_codename(): |
215 | + """Return the distro release code name, eg. 'precise' or 'trusty'.""" |
216 | + return host.lsb_release()['DISTRIB_CODENAME'] |
217 | + |
218 | + |
219 | class State(dict): |
220 | """Encapsulate state common to the unit for republishing to relations.""" |
221 | def __init__(self, state_file): |
222 | @@ -128,6 +131,7 @@ |
223 | replication_state = dict(client_state) |
224 | |
225 | add(replication_state, 'replication_password') |
226 | + add(replication_state, 'port') |
227 | add(replication_state, 'wal_received_offset') |
228 | add(replication_state, 'following') |
229 | add(replication_state, 'client_relations') |
230 | @@ -242,73 +246,101 @@ |
231 | startup_file, contents, 'postgres', 'postgres', perms=0o644) |
232 | |
233 | |
234 | -def run(command, exit_on_error=True): |
235 | +def run(command, exit_on_error=True, quiet=False): |
236 | '''Run a command and return the output.''' |
237 | - try: |
238 | - log(command, DEBUG) |
239 | - return subprocess.check_output( |
240 | - command, stderr=subprocess.STDOUT, shell=True) |
241 | - except subprocess.CalledProcessError, e: |
242 | - log("status=%d, output=%s" % (e.returncode, e.output), ERROR) |
243 | - if exit_on_error: |
244 | - sys.exit(e.returncode) |
245 | - else: |
246 | - raise |
247 | + log("Running {!r}".format(command), DEBUG) |
248 | + p = subprocess.Popen( |
249 | + command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, |
250 | + shell=isinstance(command, basestring)) |
251 | + p.stdin.close() |
252 | + lines = [] |
253 | + for line in p.stdout: |
254 | + if line: |
255 | + # LP:1274460 & LP:1259490 mean juju-log is no where near as |
256 | + # useful as we would like, so just shove a copy of the |
257 | + # output to stdout for logging. |
258 | + # log("> {}".format(line), DEBUG) |
259 | + if not quiet: |
260 | + print line |
261 | + lines.append(line) |
262 | + elif p.poll() is not None: |
263 | + break |
264 | + |
265 | + p.wait() |
266 | + |
267 | + if p.returncode == 0: |
268 | + return '\n'.join(lines) |
269 | + |
270 | + if p.returncode != 0 and exit_on_error: |
271 | + log("ERROR: {}".format(p.returncode), ERROR) |
272 | + sys.exit(p.returncode) |
273 | + |
274 | + raise subprocess.CalledProcessError( |
275 | + p.returncode, command, '\n'.join(lines)) |
276 | |
277 | |
278 | def postgresql_is_running(): |
279 | '''Return true if PostgreSQL is running.''' |
280 | - # init script always return true (9.1), add extra check to make it useful |
281 | - status, output = commands.getstatusoutput("invoke-rc.d postgresql status") |
282 | - if status != 0: |
283 | - return False |
284 | - # e.g. output: "Running clusters: 9.1/main" |
285 | - vc = "%s/%s" % (pg_version(), hookenv.config("cluster_name")) |
286 | - return vc in output.decode('utf8').split() |
287 | + for version, name, _, status in lsclusters(slice(4)): |
288 | + if (version, name) == (pg_version(), hookenv.config('cluster_name')): |
289 | + if 'online' in status.split(','): |
290 | + log('PostgreSQL is running', DEBUG) |
291 | + return True |
292 | + else: |
293 | + log('PostgreSQL is not running', DEBUG) |
294 | + return False |
295 | + assert False, 'Cluster {} {} not found'.format( |
296 | + pg_version(), hookenv.config('cluster_name')) |
297 | |
298 | |
299 | def postgresql_stop(): |
300 | '''Shutdown PostgreSQL.''' |
301 | - success = host.service_stop('postgresql') |
302 | - return not (success and postgresql_is_running()) |
303 | + if postgresql_is_running(): |
304 | + run([ |
305 | + 'pg_ctlcluster', '--force', |
306 | + pg_version(), hookenv.config('cluster_name'), 'stop']) |
307 | + log('PostgreSQL shut down') |
308 | |
309 | |
310 | def postgresql_start(): |
311 | '''Start PostgreSQL if it is not already running.''' |
312 | - success = host.service_start('postgresql') |
313 | - return success and postgresql_is_running() |
314 | + if not postgresql_is_running(): |
315 | + run([ |
316 | + 'pg_ctlcluster', pg_version(), |
317 | + hookenv.config('cluster_name'), 'start']) |
318 | + log('PostgreSQL started') |
319 | |
320 | |
321 | def postgresql_restart(): |
322 | '''Restart PostgreSQL, or start it if it is not already running.''' |
323 | if postgresql_is_running(): |
324 | with restart_lock(hookenv.local_unit(), True): |
325 | - # 'service postgresql restart' fails; it only does a reload. |
326 | - # success = host.service_restart('postgresql') |
327 | - try: |
328 | - run('pg_ctlcluster -force {} {} ' |
329 | - 'restart'.format(pg_version(), |
330 | - hookenv.config('cluster_name'))) |
331 | - success = True |
332 | - except subprocess.CalledProcessError: |
333 | - success = False |
334 | + run([ |
335 | + 'pg_ctlcluster', '--force', |
336 | + pg_version(), hookenv.config('cluster_name'), 'restart']) |
337 | + log('PostgreSQL restarted') |
338 | else: |
339 | - success = host.service_start('postgresql') |
340 | + postgresql_start() |
341 | + |
342 | + assert postgresql_is_running() |
343 | |
344 | # Store a copy of our known live configuration so |
345 | # postgresql_reload_or_restart() can make good choices. |
346 | - if success and 'saved_config' in local_state: |
347 | + if 'saved_config' in local_state: |
348 | local_state['live_config'] = local_state['saved_config'] |
349 | local_state.save() |
350 | |
351 | - return success and postgresql_is_running() |
352 | - |
353 | |
354 | def postgresql_reload(): |
355 | '''Make PostgreSQL reload its configuration.''' |
356 | # reload returns a reliable exit status |
357 | - status, output = commands.getstatusoutput("invoke-rc.d postgresql reload") |
358 | - return (status == 0) |
359 | + if postgresql_is_running(): |
360 | + # I'm using the PostgreSQL function to avoid as much indirection |
361 | + # as possible. |
362 | + success = run_select_as_postgres('SELECT pg_reload_conf()')[1][0][0] |
363 | + assert success, 'Failed to reload PostgreSQL configuration' |
364 | + log('PostgreSQL configuration reloaded') |
365 | + return postgresql_start() |
366 | |
367 | |
368 | def requires_restart(): |
369 | @@ -340,38 +372,38 @@ |
370 | # A setting has changed that requires PostgreSQL to be |
371 | # restarted before it will take effect. |
372 | restart = True |
373 | + log('{} changed from {} to {}. Restart required.'.format( |
374 | + name, live_value, new_value), DEBUG) |
375 | return restart |
376 | |
377 | |
378 | def postgresql_reload_or_restart(): |
379 | """Reload PostgreSQL configuration, restarting if necessary.""" |
380 | if requires_restart(): |
381 | - log("Configuration change requires PostgreSQL restart. Restarting.", |
382 | - WARNING) |
383 | - success = postgresql_restart() |
384 | - if not success or requires_restart(): |
385 | - log("Configuration changes failed to apply", WARNING) |
386 | - success = False |
387 | + log("Configuration change requires PostgreSQL restart", WARNING) |
388 | + postgresql_restart() |
389 | + assert not requires_restart(), "Configuration changes failed to apply" |
390 | else: |
391 | - success = host.service_reload('postgresql') |
392 | - |
393 | - if success: |
394 | - local_state['saved_config'] = local_state['live_config'] |
395 | - local_state.save() |
396 | - |
397 | - return success |
398 | - |
399 | - |
400 | -def get_service_port(config_file): |
401 | + postgresql_reload() |
402 | + |
403 | + local_state['saved_config'] = local_state['live_config'] |
404 | + local_state.save() |
405 | + |
406 | + |
407 | +def get_service_port(): |
408 | '''Return the port PostgreSQL is listening on.''' |
409 | - if not os.path.exists(config_file): |
410 | - return None |
411 | - postgresql_config = open(config_file, 'r').read() |
412 | - port = re.search("port.*=(.*)", postgresql_config).group(1).strip() |
413 | - try: |
414 | - return int(port) |
415 | - except (ValueError, TypeError): |
416 | - return None |
417 | + for version, name, port in lsclusters(slice(3)): |
418 | + if (version, name) == (pg_version(), hookenv.config('cluster_name')): |
419 | + return int(port) |
420 | + |
421 | + assert False, 'No port found for {!r} {!r}'.format( |
422 | + pg_version(), hookenv.config['cluster_name']) |
423 | + |
424 | + |
425 | +def lsclusters(s=slice(0, -1)): |
426 | + for line in run('pg_lsclusters', quiet=True).splitlines()[1:]: |
427 | + if line: |
428 | + yield line.split()[s] |
429 | |
430 | |
431 | def _get_system_ram(): |
432 | @@ -388,6 +420,8 @@ |
433 | def create_postgresql_config(config_file): |
434 | '''Create the postgresql.conf file''' |
435 | config_data = hookenv.config() |
436 | + if not config_data.get('listen_port', None): |
437 | + config_data['listen_port'] = get_service_port() |
438 | if config_data["performance_tuning"] == "auto": |
439 | # Taken from: |
440 | # http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server |
441 | @@ -439,7 +473,7 @@ |
442 | # Return it as pg_config |
443 | charm_dir = hookenv.charm_dir() |
444 | template_file = "{}/templates/postgresql.conf.tmpl".format(charm_dir) |
445 | - if not config_data['version']: |
446 | + if not config_data.get('version', None): |
447 | config_data['version'] = pg_version() |
448 | pg_config = Template( |
449 | open(template_file).read()).render(config_data) |
450 | @@ -615,7 +649,7 @@ |
451 | host.write_file(output_file, crontab_template, perms=0600) |
452 | |
453 | |
454 | -def create_recovery_conf(master_host, restart_on_change=False): |
455 | +def create_recovery_conf(master_host, master_port, restart_on_change=False): |
456 | version = pg_version() |
457 | cluster_name = hookenv.config('cluster_name') |
458 | postgresql_cluster_dir = os.path.join( |
459 | @@ -632,6 +666,7 @@ |
460 | template_file = "{}/templates/recovery.conf.tmpl".format(charm_dir) |
461 | recovery_conf = Template(open(template_file).read()).render({ |
462 | 'host': master_host, |
463 | + 'port': master_port, |
464 | 'password': local_state['replication_password'], |
465 | 'streaming_replication': streaming_replication}) |
466 | log(recovery_conf, DEBUG) |
467 | @@ -657,17 +692,16 @@ |
468 | return(None) |
469 | |
470 | |
471 | -#------------------------------------------------------------------------------ |
472 | -# update_service_ports: Convenience function that evaluate the old and new |
473 | -# service ports to decide which ports need to be |
474 | -# opened and which to close |
475 | -#------------------------------------------------------------------------------ |
476 | -def update_service_port(old_service_port=None, new_service_port=None): |
477 | - if old_service_port is None or new_service_port is None: |
478 | - return(None) |
479 | - if new_service_port != old_service_port: |
480 | - hookenv.close_port(old_service_port) |
481 | - hookenv.open_port(new_service_port) |
482 | +def update_service_port(): |
483 | + old_port = local_state.get('listen_port', None) |
484 | + new_port = get_service_port() |
485 | + if old_port != new_port: |
486 | + if new_port: |
487 | + hookenv.open_port(new_port) |
488 | + if old_port: |
489 | + hookenv.close_port(old_port) |
490 | + local_state['listen_port'] = new_port |
491 | + local_state.save() |
492 | |
493 | |
494 | def create_ssl_cert(cluster_dir): |
495 | @@ -702,12 +736,15 @@ |
496 | |
497 | |
498 | def db_cursor(autocommit=False, db='postgres', user='postgres', |
499 | - host=None, timeout=30): |
500 | + host=None, port=None, timeout=30): |
501 | import psycopg2 |
502 | + if port is None: |
503 | + port = get_service_port() |
504 | if host: |
505 | - conn_str = "dbname={} host={} user={}".format(db, host, user) |
506 | + conn_str = "dbname={} host={} port={} user={}".format( |
507 | + db, host, port, user) |
508 | else: |
509 | - conn_str = "dbname={} user={}".format(db, user) |
510 | + conn_str = "dbname={} port={} user={}".format(db, port, user) |
511 | # There are often race conditions in opening database connections, |
512 | # such as a reload having just happened to change pg_hba.conf |
513 | # settings or a hot standby being restarted and needing to catch up |
514 | @@ -750,6 +787,47 @@ |
515 | return (cur.rowcount, results) |
516 | |
517 | |
518 | +def validate_config(): |
519 | + """ |
520 | + Sanity check charm configuration, aborting the script if |
521 | + we have bogus config values or config changes the charm does not yet |
522 | + (or cannot) support. |
523 | + """ |
524 | + valid = True |
525 | + config_data = hookenv.config() |
526 | + |
527 | + version = config_data.get('version', None) |
528 | + if version: |
529 | + if version not in ('9.1', '9.2', '9.3'): |
530 | + valid = False |
531 | + log("Invalid or unsupported version {!r} requested".format( |
532 | + version), CRITICAL) |
533 | + |
534 | + if config_data['cluster_name'] != 'main': |
535 | + valid = False |
536 | + log("Cluster names other than 'main' do not work per LP:1271835", |
537 | + CRITICAL) |
538 | + |
539 | + if config_data['listen_ip'] != '*': |
540 | + valid = False |
541 | + log("listen_ip values other than '*' do not work per LP:1271837", |
542 | + CRITICAL) |
543 | + |
544 | + unchangeable_config = [ |
545 | + 'locale', 'encoding', 'version', 'cluster_name', 'pgdg'] |
546 | + |
547 | + for name in unchangeable_config: |
548 | + if (name in local_state |
549 | + and local_state[name] != config_data.get(name, None)): |
550 | + valid = False |
551 | + log("Cannot change {!r} setting after install.".format(name)) |
552 | + local_state[name] = config_data.get(name, None) |
553 | + local_state.save() |
554 | + |
555 | + if not valid: |
556 | + sys.exit(99) |
557 | + |
558 | + |
559 | #------------------------------------------------------------------------------ |
560 | # Core logic for permanent storage changes: |
561 | # NOTE the only 2 "True" return points: |
562 | @@ -869,6 +947,7 @@ |
563 | |
564 | @hooks.hook() |
565 | def config_changed(force_restart=False): |
566 | + validate_config() |
567 | config_data = hookenv.config() |
568 | update_repos_and_packages() |
569 | |
570 | @@ -908,19 +987,16 @@ |
571 | postgresql_hba = os.path.join(postgresql_config_dir, "pg_hba.conf") |
572 | postgresql_ident = os.path.join(postgresql_config_dir, "pg_ident.conf") |
573 | |
574 | - current_service_port = get_service_port(postgresql_config) |
575 | create_postgresql_config(postgresql_config) |
576 | + create_postgresql_ident(postgresql_ident) # Do this before pg_hba.conf. |
577 | generate_postgresql_hba(postgresql_hba) |
578 | - create_postgresql_ident(postgresql_ident) |
579 | create_ssl_cert(os.path.join( |
580 | postgresql_data_dir, pg_version(), config_data['cluster_name'])) |
581 | - |
582 | - updated_service_port = config_data["listen_port"] |
583 | - update_service_port(current_service_port, updated_service_port) |
584 | + update_service_port() |
585 | update_nrpe_checks() |
586 | if force_restart: |
587 | - return postgresql_restart() |
588 | - return postgresql_reload_or_restart() |
589 | + postgresql_restart() |
590 | + postgresql_reload_or_restart() |
591 | |
592 | |
593 | @hooks.hook() |
594 | @@ -930,6 +1006,8 @@ |
595 | if os.path.isfile(f) and os.access(f, os.X_OK): |
596 | subprocess.check_call(['sh', '-c', f]) |
597 | |
598 | + validate_config() |
599 | + |
600 | config_data = hookenv.config() |
601 | update_repos_and_packages() |
602 | if not 'state' in local_state: |
603 | @@ -938,14 +1016,35 @@ |
604 | # any non-idempotent setup. We should probably fix this; it |
605 | # seems rather fragile. |
606 | local_state.setdefault('state', 'standalone') |
607 | - local_state.publish() |
608 | |
609 | # Drop the cluster created when the postgresql package was |
610 | # installed, and rebuild it with the requested locale and encoding. |
611 | version = pg_version() |
612 | - run("pg_dropcluster --stop {} main".format(version)) |
613 | - run("pg_createcluster --locale='{}' --encoding='{}' {} main".format( |
614 | - config_data['locale'], config_data['encoding'], version)) |
615 | + for ver, name in lsclusters(slice(2)): |
616 | + if version == ver and name == 'main': |
617 | + run("pg_dropcluster --stop {} main".format(version)) |
618 | + listen_port = config_data.get('listen_port', None) |
619 | + if listen_port: |
620 | + port_opt = "--port={}".format(config_data['listen_port']) |
621 | + else: |
622 | + port_opt = '' |
623 | + with switch_cwd('/tmp'): |
624 | + create_cmd = [ |
625 | + "pg_createcluster", |
626 | + "--locale", config_data['locale'], |
627 | + "-e", config_data['encoding']] |
628 | + if listen_port: |
629 | + create_cmd.extend(["-p", str(config_data['listen_port'])]) |
630 | + create_cmd.append(pg_version()) |
631 | + create_cmd.append(config_data['cluster_name']) |
632 | + run(create_cmd) |
633 | + assert ( |
634 | + not port_opt |
635 | + or get_service_port() == config_data['listen_port']), ( |
636 | + 'allocated port {!r} != {!r}'.format( |
637 | + get_service_port(), config_data['listen_port'])) |
638 | + local_state['port'] = get_service_port() |
639 | + local_state.publish() |
640 | |
641 | postgresql_backups_dir = ( |
642 | config_data['backup_dir'].strip() or |
643 | @@ -972,7 +1071,7 @@ |
644 | '{}/pg_backup_job'.format(postgresql_scripts_dir), |
645 | backup_job, perms=0755) |
646 | install_postgresql_crontab(postgresql_crontab) |
647 | - hookenv.open_port(5432) |
648 | + hookenv.open_port(get_service_port()) |
649 | |
650 | # Ensure at least minimal access granted for hooks to run. |
651 | # Reload because we are using the default cluster setup and started |
652 | @@ -990,16 +1089,14 @@ |
653 | |
654 | @hooks.hook() |
655 | def start(): |
656 | - if not postgresql_reload_or_restart(): |
657 | - raise SystemExit(1) |
658 | + postgresql_reload_or_restart() |
659 | |
660 | |
661 | @hooks.hook() |
662 | def stop(): |
663 | if postgresql_is_running(): |
664 | with restart_lock(hookenv.local_unit(), True): |
665 | - if not postgresql_stop(): |
666 | - raise SystemExit(1) |
667 | + postgresql_stop() |
668 | |
669 | |
670 | def quote_identifier(identifier): |
671 | @@ -1262,7 +1359,7 @@ |
672 | schema_password = create_user(schema_user) |
673 | ensure_database(user, schema_user, database) |
674 | host = hookenv.unit_private_ip() |
675 | - port = hookenv.config('listen_port') |
676 | + port = get_service_port() |
677 | state = local_state['state'] # master, hot standby, standalone |
678 | |
679 | # Publish connection details. |
680 | @@ -1301,7 +1398,7 @@ |
681 | |
682 | password = create_user(user, admin=True) |
683 | host = hookenv.unit_private_ip() |
684 | - port = hookenv.config('listen_port') |
685 | + port = get_service_port() |
686 | state = local_state['state'] # master, hot standby, standalone |
687 | |
688 | # Publish connection details. |
689 | @@ -1386,18 +1483,62 @@ |
690 | |
691 | |
692 | def update_repos_and_packages(): |
693 | - extra_repos = hookenv.config('extra_archives') |
694 | - extra_repos_added = local_state.setdefault('extra_repos_added', set()) |
695 | - if extra_repos: |
696 | - repos_added = False |
697 | - for repo in extra_repos.split(): |
698 | - if repo not in extra_repos_added: |
699 | - fetch.add_source(repo) |
700 | - extra_repos_added.add(repo) |
701 | - repos_added = True |
702 | - if repos_added: |
703 | - fetch.apt_update(fatal=True) |
704 | - local_state.save() |
705 | + need_upgrade = False |
706 | + |
707 | + # Add the PGDG APT repository if it is enabled. Setting this boolean |
708 | + # is simpler than requiring the magic URL and key be added to |
709 | + # install_sources and install_keys. In addition, per Bug #1271148, |
710 | + # install_keys is likely a security hole for this sort of remote |
711 | + # archive. Instead, we keep a copy of the signing key in the charm |
712 | + # and can add it securely. |
713 | + pgdg_list = '/etc/apt/sources.list.d/pgdg_{}.list'.format( |
714 | + sanitize(hookenv.local_unit())) |
715 | + pgdg_key = 'ACCC4CF8' |
716 | + |
717 | + if hookenv.config('pgdg'): |
718 | + if not os.path.exists(pgdg_list): |
719 | + # We need to upgrade, as if we have Ubuntu main packages |
720 | + # installed they may be incompatible with the PGDG ones. |
721 | + # This is unlikely to ever happen outside of the test suite, |
722 | + # and never if you don't reuse machines. |
723 | + need_upgrade = True |
724 | + run("apt-key add lib/{}.asc".format(pgdg_key)) |
725 | + open(pgdg_list, 'w').write('deb {} {}-pgdg main'.format( |
726 | + 'http://apt.postgresql.org/pub/repos/apt/', distro_codename())) |
727 | + elif os.path.exists(pgdg_list): |
728 | + log( |
729 | + "PGDG apt source not requested, but already in place in this " |
730 | + "container", WARNING) |
731 | + # We can't just remove a source, as we may have packages |
732 | + # installed that conflict with ones from the other configured |
733 | + # sources. In particular, if we have postgresql-common installed |
734 | + # from the PGDG Apt source, PostgreSQL packages from Ubuntu main |
735 | + # will fail to install. |
736 | + # os.unlink(pgdg_list) |
737 | + |
738 | + # Try to optimize our calls to fetch.configure_sources(), as it |
739 | + # cannot do this itself due to lack of state. |
740 | + if (need_upgrade |
741 | + or local_state.get('install_sources', None) |
742 | + != hookenv.config('install_sources') |
743 | + or local_state.get('install_keys', None) |
744 | + != hookenv.config('install_keys')): |
745 | + # Support the standard mechanism implemented by charm-helpers. Pulls |
746 | + # from the default 'install_sources' and 'install_keys' config |
747 | + # options. This also does 'apt-get update', pulling in the PGDG data |
748 | + # if we just configured it. |
749 | + fetch.configure_sources(True) |
750 | + local_state['install_sources'] = hookenv.config('install_sources') |
751 | + local_state['install_keys'] = hookenv.config('install_keys') |
752 | + local_state.save() |
753 | + |
754 | + # Ensure that the desired database locale is possible. |
755 | + if hookenv.config('locale') != 'C': |
756 | + run(["locale-gen", "{}.{}".format( |
757 | + hookenv.config('locale'), hookenv.config('encoding'))]) |
758 | + |
759 | + if need_upgrade: |
760 | + run("apt-get -y upgrade") |
761 | |
762 | version = pg_version() |
763 | # It might have been better for debversion and plpython to only get |
764 | @@ -1405,11 +1546,14 @@ |
765 | # but they predate this feature. |
766 | packages = ["python-psutil", # to obtain system RAM from python |
767 | "libc-bin", # for getconf |
768 | - "postgresql-%s" % version, |
769 | - "postgresql-contrib-%s" % version, |
770 | - "postgresql-plpython-%s" % version, |
771 | - "postgresql-%s-debversion" % version, |
772 | + "postgresql-{}".format(version), |
773 | + "postgresql-contrib-{}".format(version), |
774 | + "postgresql-plpython-{}".format(version), |
775 | "python-jinja2", "syslinux", "python-psycopg2"] |
776 | + # PGDG currently doesn't have debversion for 9.3. Put this back when |
777 | + # it does. |
778 | + if not (hookenv.config('pgdg') and version == '9.3'): |
779 | + "postgresql-{}-debversion".format(version) |
780 | packages.extend((hookenv.config('extra-packages') or '').split()) |
781 | packages = fetch.filter_installed_packages(packages) |
782 | fetch.apt_install(packages, fatal=True) |
783 | @@ -1472,7 +1616,8 @@ |
784 | '''Connect the database as a streaming replica of the master.''' |
785 | master_relation = hookenv.relation_get(unit=master) |
786 | create_recovery_conf( |
787 | - master_relation['private-address'], restart_on_change=True) |
788 | + master_relation['private-address'], |
789 | + master_relation['port'], restart_on_change=True) |
790 | |
791 | |
792 | def elected_master(): |
793 | @@ -1618,6 +1763,7 @@ |
794 | local_state['replication_password'] = replication_password |
795 | local_state['client_relations'] = ' '.join( |
796 | hookenv.relation_ids('db') + hookenv.relation_ids('db-admin')) |
797 | + local_state.publish() |
798 | |
799 | else: |
800 | log("I am master and remain master") |
801 | @@ -1635,8 +1781,10 @@ |
802 | master)) |
803 | |
804 | master_ip = hookenv.relation_get('private-address', master) |
805 | + master_port = hookenv.relation_get('port', master) |
806 | + assert master_port is not None, 'No master port set' |
807 | |
808 | - clone_database(master, master_ip) |
809 | + clone_database(master, master_ip, master_port) |
810 | |
811 | local_state['state'] = 'hot standby' |
812 | local_state['following'] = master |
813 | @@ -1711,7 +1859,7 @@ |
814 | |
815 | # Override unit specific connection details |
816 | connection_settings['host'] = hookenv.unit_private_ip() |
817 | - connection_settings['port'] = hookenv.config('listen_port') |
818 | + connection_settings['port'] = get_service_port() |
819 | connection_settings['state'] = local_state['state'] |
820 | |
821 | # Block until users and database has replicated, so we know the |
822 | @@ -1833,9 +1981,10 @@ |
823 | cur = db_cursor(autocommit=True) |
824 | else: |
825 | host = hookenv.relation_get('private-address', unit) |
826 | + port = hookenv.relation_get('port', unit) |
827 | cur = db_cursor( |
828 | - autocommit=True, db='postgres', |
829 | - user='juju_replication', host=host) |
830 | + autocommit=True, db='postgres', user='juju_replication', |
831 | + host=host, port=port) |
832 | cur.execute(q) |
833 | break |
834 | except psycopg2.Error: |
835 | @@ -1853,7 +2002,7 @@ |
836 | pass |
837 | |
838 | |
839 | -def clone_database(master_unit, master_host): |
840 | +def clone_database(master_unit, master_host, master_port): |
841 | with restart_lock(master_unit, False): |
842 | postgresql_stop() |
843 | log("Cloning master {}".format(master_unit)) |
844 | @@ -1868,7 +2017,8 @@ |
845 | 'sudo', '-E', # -E needed to locate pgpass file. |
846 | '-u', 'postgres', 'pg_basebackup', '-D', postgresql_cluster_dir, |
847 | '--xlog', '--checkpoint=fast', '--no-password', |
848 | - '-h', master_host, '-p', '5432', '--username=juju_replication'] |
849 | + '-h', master_host, '-p', master_port, |
850 | + '--username=juju_replication'] |
851 | log(' '.join(cmd), DEBUG) |
852 | |
853 | if os.path.isdir(postgresql_cluster_dir): |
854 | @@ -1883,7 +2033,7 @@ |
855 | log(output, DEBUG) |
856 | # Debian by default expects SSL certificates in the datadir. |
857 | create_ssl_cert(postgresql_cluster_dir) |
858 | - create_recovery_conf(master_host) |
859 | + create_recovery_conf(master_host, master_port) |
860 | except subprocess.CalledProcessError as x: |
861 | # We failed, and this cluster is broken. Rebuild a |
862 | # working cluster so start/stop etc. works and we |
863 | @@ -1955,9 +2105,10 @@ |
864 | return int(logid, 16) * 16 * 1024 * 1024 * 255 + int(offset, 16) |
865 | |
866 | |
867 | -def wait_for_db(timeout=120, db='postgres', user='postgres', host=None): |
868 | +def wait_for_db( |
869 | + timeout=120, db='postgres', user='postgres', host=None, port=None): |
870 | '''Wait until the db is fully up.''' |
871 | - db_cursor(db=db, user=user, host=host, timeout=timeout) |
872 | + db_cursor(db=db, user=user, host=host, port=port, timeout=timeout) |
873 | |
874 | |
875 | def unit_sorted(units): |
876 | @@ -2016,7 +2167,7 @@ |
877 | nrpe_check_config.write("# check pgsql\n") |
878 | nrpe_check_config.write( |
879 | "command[check_pgsql]=/usr/lib/nagios/plugins/check_pgsql -P {}" |
880 | - .format(config_data['listen_port'])) |
881 | + .format(get_service_port())) |
882 | # pgsql backups |
883 | nrpe_check_file = '/etc/nagios/nrpe.d/check_pgsql_backups.cfg' |
884 | backup_log = "{}/backups.log".format(postgresql_logs_dir) |
885 | |
886 | === added directory 'lib' |
887 | === added file 'lib/ACCC4CF8.asc' |
888 | --- lib/ACCC4CF8.asc 1970-01-01 00:00:00 +0000 |
889 | +++ lib/ACCC4CF8.asc 2014-02-07 12:21:04 +0000 |
890 | @@ -0,0 +1,45 @@ |
891 | +This is the signing key for the PostgreSQL Global Development Group |
892 | +APT repository. See https://wiki.postgresql.org/wiki/Apt for details. |
893 | + |
894 | +-----BEGIN PGP PUBLIC KEY BLOCK----- |
895 | +Version: GnuPG v1.4.12 (GNU/Linux) |
896 | + |
897 | +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 |
933 | +VPZ67j5UMxqtT/Y4+NHcQGgwF/1i |
934 | +=Iugu |
935 | +-----END PGP PUBLIC KEY BLOCK----- |
936 | |
937 | === modified file 'templates/pg_hba.conf.tmpl' |
938 | --- templates/pg_hba.conf.tmpl 2013-11-19 12:51:43 +0000 |
939 | +++ templates/pg_hba.conf.tmpl 2014-02-07 12:21:04 +0000 |
940 | @@ -3,7 +3,7 @@ |
941 | #------------------------------------------------------------------------------ |
942 | |
943 | # Database administrative login by UNIX sockets |
944 | -local all postgres ident map=superusers |
945 | +local all root,postgres ident map=superusers |
946 | local replication root,postgres ident map=superusers |
947 | local all nagios md5 |
948 | |
949 | |
950 | === modified file 'templates/pg_ident.conf.tmpl' |
951 | --- templates/pg_ident.conf.tmpl 2012-10-05 13:21:43 +0000 |
952 | +++ templates/pg_ident.conf.tmpl 2014-02-07 12:21:04 +0000 |
953 | @@ -1,2 +1,3 @@ |
954 | +# This file is managed by juju |
955 | superusers root postgres |
956 | superusers postgres postgres |
957 | |
958 | === modified file 'templates/postgresql.conf.tmpl' |
959 | --- templates/postgresql.conf.tmpl 2014-01-17 03:18:52 +0000 |
960 | +++ templates/postgresql.conf.tmpl 2014-02-07 12:21:04 +0000 |
961 | @@ -23,6 +23,11 @@ |
962 | unix_socket_directory = '/var/run/postgresql' |
963 | {% endif -%} |
964 | |
965 | +{% if version >= "9.2" -%} |
966 | +ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' |
967 | +ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' |
968 | +{% endif -%} |
969 | + |
970 | {% if listen_ip != "" -%} |
971 | listen_addresses = '{{listen_ip}}' |
972 | {% endif -%} |
973 | |
974 | === modified file 'templates/recovery.conf.tmpl' |
975 | --- templates/recovery.conf.tmpl 2013-11-28 08:43:35 +0000 |
976 | +++ templates/recovery.conf.tmpl 2014-02-07 12:21:04 +0000 |
977 | @@ -4,7 +4,7 @@ |
978 | standby_mode = on |
979 | recovery_target_timeline = latest |
980 | {% if streaming_replication %} |
981 | -primary_conninfo = 'host={{host}} user=juju_replication password={{password}} requirepeer=postgres' |
982 | +primary_conninfo = 'host={{host}} port={{port}} user=juju_replication password={{password}} requirepeer=postgres' |
983 | {% endif %} |
984 | {% if restore_command %} |
985 | restore_command = '{{restore_command}}' |
986 | |
987 | === modified file 'test.py' |
988 | --- test.py 2013-12-23 10:38:13 +0000 |
989 | +++ test.py 2014-02-07 12:21:04 +0000 |
990 | @@ -25,13 +25,30 @@ |
991 | |
992 | SERIES = 'precise' |
993 | TEST_CHARM = 'local:postgresql' |
994 | -PSQL_CHARM = 'local:postgresql-psql' |
995 | - |
996 | - |
997 | -class PostgreSQLCharmTestCase(testtools.TestCase, fixtures.TestWithFixtures): |
998 | +PSQL_CHARM = 'cs:postgresql-psql' |
999 | + |
1000 | + |
1001 | +class PostgreSQLCharmBaseTestCase(object): |
1002 | + |
1003 | + # Override these in subclasses to run these tests multiple times |
1004 | + # for different PostgreSQL versions. |
1005 | + |
1006 | + # PostgreSQL version for tests. One of the subclasses leaves the |
1007 | + # VERSION as None to test automatic version selection. |
1008 | + VERSION = None |
1009 | + |
1010 | + # Use the PGDG Apt archive or not. One of the subclasses sets this |
1011 | + # to False to test the Ubuntu main packages. The rest set this to |
1012 | + # True to pull packages from the PGDG (only one PostgreSQL version |
1013 | + # exists in main). |
1014 | + PGDG = None |
1015 | |
1016 | def setUp(self): |
1017 | - super(PostgreSQLCharmTestCase, self).setUp() |
1018 | + super(PostgreSQLCharmBaseTestCase, self).setUp() |
1019 | + |
1020 | + # Generate a basic config for all PostgreSQL charm deploys. |
1021 | + # Tests may add or change options. |
1022 | + self.pg_config = dict(version=self.VERSION, pgdg=self.PGDG) |
1023 | |
1024 | self.juju = self.useFixture(JujuFixture( |
1025 | reuse_machines=True, |
1026 | @@ -99,7 +116,7 @@ |
1027 | |
1028 | def test_basic(self): |
1029 | '''Connect to a a single unit service via the db relationship.''' |
1030 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1031 | + self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config) |
1032 | self.juju.deploy(PSQL_CHARM, 'psql') |
1033 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1034 | self.juju.wait_until_ready() |
1035 | @@ -107,11 +124,27 @@ |
1036 | result = self.sql('SELECT TRUE') |
1037 | self.assertEqual(result, [['t']]) |
1038 | |
1039 | + def test_streaming_replication(self): |
1040 | + self.juju.deploy( |
1041 | + TEST_CHARM, 'postgresql', num_units=2, config=self.pg_config) |
1042 | + self.juju.deploy(PSQL_CHARM, 'psql') |
1043 | + self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1044 | + self.juju.wait_until_ready() |
1045 | + |
1046 | + # Confirm that the slave has successfully opened a streaming |
1047 | + # replication connection. |
1048 | + num_slaves = self.sql( |
1049 | + 'SELECT COUNT(*) FROM pg_stat_replication', |
1050 | + postgres_unit='master')[0][0] |
1051 | + |
1052 | + self.assertEqual(num_slaves, '1', 'Slave not connected') |
1053 | + |
1054 | def test_basic_admin(self): |
1055 | '''Connect to a single unit service via the db-admin relationship.''' |
1056 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1057 | + self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config) |
1058 | self.juju.deploy(PSQL_CHARM, 'psql') |
1059 | self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin']) |
1060 | + self.juju.do(['expose', 'postgresql']) |
1061 | self.juju.wait_until_ready() |
1062 | |
1063 | result = self.sql('SELECT TRUE', dbname='postgres') |
1064 | @@ -125,7 +158,8 @@ |
1065 | |
1066 | def test_failover(self): |
1067 | """Set up a multi-unit service and perform failovers.""" |
1068 | - self.juju.deploy(TEST_CHARM, 'postgresql', num_units=3) |
1069 | + self.juju.deploy( |
1070 | + TEST_CHARM, 'postgresql', num_units=3, config=self.pg_config) |
1071 | self.juju.deploy(PSQL_CHARM, 'psql') |
1072 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1073 | self.juju.wait_until_ready() |
1074 | @@ -213,7 +247,8 @@ |
1075 | |
1076 | def test_failover_election(self): |
1077 | """Ensure master elected in a failover is the best choice""" |
1078 | - self.juju.deploy(TEST_CHARM, 'postgresql', num_units=3) |
1079 | + self.juju.deploy( |
1080 | + TEST_CHARM, 'postgresql', num_units=3, config=self.pg_config) |
1081 | self.juju.deploy(PSQL_CHARM, 'psql') |
1082 | self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin']) |
1083 | self.juju.wait_until_ready() |
1084 | @@ -261,7 +296,15 @@ |
1085 | self.assertIs(False, self.is_master(standby_unit_1, 'postgres')) |
1086 | |
1087 | def test_admin_addresses(self): |
1088 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1089 | + |
1090 | + # This test also tests explicit port assignment. We need |
1091 | + # a different port for each PostgreSQL version we might be |
1092 | + # testing, because clusters from previous tests of different |
1093 | + # versions may be hanging around. |
1094 | + port = 7400 + int((self.VERSION or '66').replace('.', '')) |
1095 | + self.pg_config['listen_port'] = port |
1096 | + |
1097 | + self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config) |
1098 | self.juju.deploy(PSQL_CHARM, 'psql') |
1099 | self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin']) |
1100 | self.juju.wait_until_ready() |
1101 | @@ -271,7 +314,7 @@ |
1102 | unit_ip = self.juju.status['services']['postgresql']['units'][ |
1103 | unit]['public-address'] |
1104 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
1105 | - s.connect((unit_ip, 5432)) |
1106 | + s.connect((unit_ip, port)) |
1107 | my_ip = s.getsockname()[0] |
1108 | del s |
1109 | |
1110 | @@ -280,8 +323,9 @@ |
1111 | "ALTER USER postgres ENCRYPTED PASSWORD 'foo'", dbname='postgres') |
1112 | |
1113 | # Direct connection string to the unit's database. |
1114 | - conn_str = 'dbname=postgres user=postgres password=foo host={}'.format( |
1115 | - unit_ip) |
1116 | + conn_str = ( |
1117 | + 'dbname=postgres user=postgres password=foo ' |
1118 | + 'host={} port={}'.format(unit_ip, port)) |
1119 | |
1120 | # Direct database connections should fail at the moment. |
1121 | self.assertRaises( |
1122 | @@ -297,7 +341,7 @@ |
1123 | self.assertEquals(1, cur.fetchone()[0]) |
1124 | |
1125 | def test_explicit_database(self): |
1126 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1127 | + self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config) |
1128 | self.juju.deploy(PSQL_CHARM, 'psql') |
1129 | self.juju.do(['set', 'psql', 'database=explicit']) |
1130 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1131 | @@ -306,11 +350,12 @@ |
1132 | result = self.sql('SELECT current_database()') |
1133 | self.assertEqual(result, [['explicit']]) |
1134 | |
1135 | - |
1136 | def test_roles_granted(self): |
1137 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1138 | - self.juju.deploy(PSQL_CHARM, 'psql') |
1139 | - self.juju.do(['set', 'psql', 'roles=role_a']) |
1140 | + # We use two units to confirm that there is no attempts to |
1141 | + # grant roles on the hot standby. |
1142 | + self.juju.deploy( |
1143 | + TEST_CHARM, 'postgresql', num_units=2, config=self.pg_config) |
1144 | + self.juju.deploy(PSQL_CHARM, 'psql', config={'roles': 'role_a'}) |
1145 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1146 | self.juju.wait_until_ready() |
1147 | |
1148 | @@ -330,9 +375,11 @@ |
1149 | self.assertEqual(result, [['t', 't']]) |
1150 | |
1151 | def test_roles_revoked(self): |
1152 | - self.juju.deploy(TEST_CHARM, 'postgresql') |
1153 | - self.juju.deploy(PSQL_CHARM, 'psql') |
1154 | - self.juju.do(['set', 'psql', 'roles=role_a,role_b']) |
1155 | + # We use two units to confirm that there is no attempts to |
1156 | + # grant roles on the hot standby. |
1157 | + self.juju.deploy( |
1158 | + TEST_CHARM, 'postgresql', num_units=2, config=self.pg_config) |
1159 | + self.juju.deploy(PSQL_CHARM, 'psql', config={'roles': 'role_a,role_b'}) |
1160 | self.juju.do(['add-relation', 'postgresql:db', 'psql:db']) |
1161 | self.juju.wait_until_ready() |
1162 | |
1163 | @@ -366,6 +413,29 @@ |
1164 | self.assertEqual(result, [['f', 'f', 'f']]) |
1165 | |
1166 | |
1167 | +class PG91Tests( |
1168 | + PostgreSQLCharmBaseTestCase, |
1169 | + testtools.TestCase, fixtures.TestWithFixtures): |
1170 | + # Test automatic version selection under precise. |
1171 | + VERSION = None if SERIES == 'precise' else '9.1' |
1172 | + PGDG = False if SERIES == 'precise' else True |
1173 | + |
1174 | + |
1175 | +class PG92Tests( |
1176 | + PostgreSQLCharmBaseTestCase, |
1177 | + testtools.TestCase, fixtures.TestWithFixtures): |
1178 | + VERSION = '9.2' |
1179 | + PGDG = True |
1180 | + |
1181 | + |
1182 | +class PG93Tests( |
1183 | + PostgreSQLCharmBaseTestCase, |
1184 | + testtools.TestCase, fixtures.TestWithFixtures): |
1185 | + # Test automatic version selection under trusty. |
1186 | + VERSION = None if SERIES == 'trusty' else '9.3' |
1187 | + PGDG = False if SERIES == 'trusty' else True |
1188 | + |
1189 | + |
1190 | def unit_sorted(units): |
1191 | """Return a correctly sorted list of unit names.""" |
1192 | return sorted( |
1193 | |
1194 | === modified file 'testing/jujufixture.py' |
1195 | --- testing/jujufixture.py 2013-12-23 10:38:13 +0000 |
1196 | +++ testing/jujufixture.py 2014-02-07 12:21:04 +0000 |
1197 | @@ -1,9 +1,11 @@ |
1198 | import json |
1199 | +import os.path |
1200 | import subprocess |
1201 | import time |
1202 | |
1203 | import fixtures |
1204 | from testtools.content import text_content |
1205 | +import yaml |
1206 | |
1207 | |
1208 | __all__ = ['JujuFixture', 'run'] |
1209 | @@ -42,7 +44,7 @@ |
1210 | return json.loads(out) |
1211 | return None |
1212 | |
1213 | - def deploy(self, charm, name=None, num_units=1): |
1214 | + def deploy(self, charm, name=None, num_units=1, config=None): |
1215 | # The first time we deploy a local: charm in the test run, it |
1216 | # needs to deploy with --update to ensure we are testing the |
1217 | # desired revision of the charm. Subsequent deploys we do not |
1218 | @@ -54,6 +56,14 @@ |
1219 | cmd = ['deploy', '-u'] |
1220 | self._deployed_charms.add(charm) |
1221 | |
1222 | + if config: |
1223 | + config_path = os.path.join( |
1224 | + self.useFixture(fixtures.TempDir()).path, 'config.yaml') |
1225 | + cmd.append('--config={}'.format(config_path)) |
1226 | + config = yaml.safe_dump({name: config}, default_flow_style=False) |
1227 | + open(config_path, 'w').write(config) |
1228 | + self.addDetail('pgconfig', text_content(config)) |
1229 | + |
1230 | cmd.append(charm) |
1231 | |
1232 | if name is None: |
1233 | @@ -93,10 +103,10 @@ |
1234 | self.status = self.get_result(['status']) |
1235 | |
1236 | self._free_machines = set( |
1237 | - int(k) for k, m in self.status['machines'].items() if |
1238 | - k != '0' |
1239 | - and m.get('life', None) not in ('dead', 'dying') |
1240 | - and m.get('agent-state', 'pending') in ('started', 'ready')) |
1241 | + int(k) for k, m in self.status['machines'].items() |
1242 | + if k != '0' |
1243 | + and m.get('life', None) not in ('dead', 'dying') |
1244 | + and m.get('agent-state', 'pending') in ('started', 'ready')) |
1245 | for service in self.status.get('services', {}).values(): |
1246 | for unit in service.get('units', []): |
1247 | if 'machine' in unit: |
1248 | @@ -201,6 +211,8 @@ |
1249 | raise |
1250 | |
1251 | (out, err) = proc.communicate(input) |
1252 | + detail_collector.addDetail( |
1253 | + 'cmd', text_content('{}: {}'.format(proc.returncode, ' '.join(cmd)))) |
1254 | if out: |
1255 | detail_collector.addDetail('stdout', text_content(out)) |
1256 | if err: |
1257 | |
1258 | === added file 'tests/00_setup.test' |
1259 | --- tests/00_setup.test 1970-01-01 00:00:00 +0000 |
1260 | +++ tests/00_setup.test 2014-02-07 12:21:04 +0000 |
1261 | @@ -0,0 +1,12 @@ |
1262 | +#!/bin/sh |
1263 | + |
1264 | +sudo apt-get install -y \ |
1265 | + python-flake8 \ |
1266 | + python-fixtures \ |
1267 | + python-jinja2 \ |
1268 | + python-mocker \ |
1269 | + python-psycopg2 \ |
1270 | + python-testtools \ |
1271 | + python-twisted-core \ |
1272 | + python-yaml |
1273 | + |
1274 | |
1275 | === added file 'tests/01_lint.test' |
1276 | --- tests/01_lint.test 1970-01-01 00:00:00 +0000 |
1277 | +++ tests/01_lint.test 2014-02-07 12:21:04 +0000 |
1278 | @@ -0,0 +1,3 @@ |
1279 | +#!/bin/bash |
1280 | + |
1281 | +make -C $(dirname $0)/.. lint |
1282 | |
1283 | === renamed file 'tests/01_unittests.test' => 'tests/02_unit_test.test' |
1284 | --- tests/01_unittests.test 2014-01-15 10:18:42 +0000 |
1285 | +++ tests/02_unit_test.test 2014-02-07 12:21:04 +0000 |
1286 | @@ -1,3 +1,3 @@ |
1287 | #!/bin/bash |
1288 | |
1289 | -cd $(dirname $0)/../hooks && trial test_hooks.py |
1290 | +make -C $(dirname $0)/.. unit_test |
1291 | |
1292 | === renamed symlink 'tests/10_juju_integration.test' => 'tests/91_integration_test_91.test' (properties changed: -x to +x) |
1293 | === target was u'../test.py' |
1294 | --- tests/10_juju_integration.test 1970-01-01 00:00:00 +0000 |
1295 | +++ tests/91_integration_test_91.test 2014-02-07 12:21:04 +0000 |
1296 | @@ -0,0 +1,3 @@ |
1297 | +#!/bin/bash |
1298 | + |
1299 | +make -C $(dirname $0)/.. integration_test_91 |
1300 | |
1301 | === added file 'tests/92_integration_test_92.test' |
1302 | --- tests/92_integration_test_92.test 1970-01-01 00:00:00 +0000 |
1303 | +++ tests/92_integration_test_92.test 2014-02-07 12:21:04 +0000 |
1304 | @@ -0,0 +1,3 @@ |
1305 | +#!/bin/bash |
1306 | + |
1307 | +make -C $(dirname $0)/.. integration_test_92 |
1308 | |
1309 | === added file 'tests/93_integration_test_93.test' |
1310 | --- tests/93_integration_test_93.test 1970-01-01 00:00:00 +0000 |
1311 | +++ tests/93_integration_test_93.test 2014-02-07 12:21:04 +0000 |
1312 | @@ -0,0 +1,3 @@ |
1313 | +#!/bin/bash |
1314 | + |
1315 | +make -C $(dirname $0)/.. integration_test_93 |
Fantastic work, thank you for submitting this.
make test fails however with the following:
Lint check (flake8) py:2109: 5: E125 continuation line does not distinguish itself from next logical line jujufixture. py
directory hooks
checking hooks/helpers.py
checking hooks/hooks.py
hooks/hooks.
checking hooks/test_hooks.py
directory testing
checking testing/__init__.py
checking testing/
directory tests
checking test.py
make: *** [test] Error 1
Seems simple enough, otherwise LGTM