Merge ~stub/postgresql-charm:upgrade-series into postgresql-charm:master
- Git
- lp:~stub/postgresql-charm
- upgrade-series
- Merge into master
Proposed by
Stuart Bishop
Status: | Merged |
---|---|
Merged at revision: | a1d109cbbfb999f9c5715e268cd0226668f7d8cf |
Proposed branch: | ~stub/postgresql-charm:upgrade-series |
Merge into: | postgresql-charm:master |
Diff against target: |
476 lines (+114/-125) 7 files modified
Makefile (+2/-19) reactive/postgresql/postgresql.py (+21/-4) reactive/postgresql/service.py (+32/-9) reactive/postgresql/upgrade.py (+6/-0) requirements.txt (+3/-2) tox.ini (+4/-3) unit_tests/test_postgresql.py (+46/-88) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Price | Approve | ||
Canonical IS Reviewers | Pending | ||
PostgreSQL Charm Maintainers | Pending | ||
Review via email: mp+395355@code.launchpad.net |
Commit message
upgrade-series support, resurrect unit tests
The charm would already probably survive a series upgrade, but
these changes should ensure the service always gets back up on
its feet. PostgreSQL major version upgrades might even work too,
although that would be better left to an action.
Also gets the unit tests back running, which had atrophied.
They are still rather awkward to run (they need to run in the
built charm directory), but are passing again.
Description of the change
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Revision history for this message
Barry Price (barryprice) wrote : | # |
Couple of nitpick questions on the Makefile, otherwise LGTM
review:
Approve
Revision history for this message
Barry Price (barryprice) wrote : | # |
Actually save the second of those questions :)
Revision history for this message
Stuart Bishop (stub) : | # |
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change cannot be self approved, setting status to needs review.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/Makefile b/Makefile | |||
2 | index b05ce5b..089e02e 100644 | |||
3 | --- a/Makefile | |||
4 | +++ b/Makefile | |||
5 | @@ -27,24 +27,7 @@ _success_ex: | |||
6 | 27 | true | ts | 27 | true | ts |
7 | 28 | 28 | ||
8 | 29 | 29 | ||
27 | 30 | # Munge the path so the requested version of Juju is found, and thus used | 30 | test: lint unittest integration |
10 | 31 | # by Amulet and juju-deployer. | ||
11 | 32 | export PATH := /usr/lib/juju-$(shell $(JUJU) --version | perl -p -e "s/-.*//")/bin:$(PATH) | ||
12 | 33 | |||
13 | 34 | |||
14 | 35 | test: testdeps lint unittest integration | ||
15 | 36 | |||
16 | 37 | testdeps: | ||
17 | 38 | ifeq ($(HOST_SERIES),trusty) | ||
18 | 39 | sudo apt-get install -y python-tox python3-psycopg2 bzr moreutils \ | ||
19 | 40 | software-properties-common | ||
20 | 41 | else | ||
21 | 42 | sudo apt-get install -y tox python3-psycopg2 bzr moreutils \ | ||
22 | 43 | software-properties-common | ||
23 | 44 | endif | ||
24 | 45 | sudo add-apt-repository -y ppa:juju/stable | ||
25 | 46 | sudo apt-get install charm-tools | ||
26 | 47 | |||
28 | 48 | 31 | ||
29 | 49 | CHARM_NAME := postgresql | 32 | CHARM_NAME := postgresql |
30 | 50 | 33 | ||
31 | @@ -69,7 +52,7 @@ $(BUILD_DIR): | |||
32 | 69 | # updates. | 52 | # updates. |
33 | 70 | .PHONY: build | 53 | .PHONY: build |
34 | 71 | build: | $(BUILD_DIR) | 54 | build: | $(BUILD_DIR) |
36 | 72 | charm build -f -o $(BUILD_ROOT) -n $(CHARM_NAME) | 55 | charm build -f -d $(BUILD_DIR)/.. -n $(CHARM_NAME) |
37 | 73 | 56 | ||
38 | 74 | # Generate a fresh development build and commit it to $(TEST_BRANCH). | 57 | # Generate a fresh development build and commit it to $(TEST_BRANCH). |
39 | 75 | # Only builds work committed to $(LAYER_BRANCH). | 58 | # Only builds work committed to $(LAYER_BRANCH). |
40 | diff --git a/reactive/postgresql/postgresql.py b/reactive/postgresql/postgresql.py | |||
41 | index 373080d..35d3e02 100644 | |||
42 | --- a/reactive/postgresql/postgresql.py | |||
43 | +++ b/reactive/postgresql/postgresql.py | |||
44 | @@ -79,6 +79,13 @@ def version(): | |||
45 | 79 | if version: | 79 | if version: |
46 | 80 | return version | 80 | return version |
47 | 81 | 81 | ||
48 | 82 | # If no cached version, use the version reported by | ||
49 | 83 | # pg_lsclusters | ||
50 | 84 | version = lsclusters_version() | ||
51 | 85 | if version: | ||
52 | 86 | unitdata.kv().set("postgresql.pg_version", version) | ||
53 | 87 | return version | ||
54 | 88 | |||
55 | 82 | # We use the charm configuration here, as multiple versions | 89 | # We use the charm configuration here, as multiple versions |
56 | 83 | # of PostgreSQL may be installed. | 90 | # of PostgreSQL may be installed. |
57 | 84 | config = hookenv.config() | 91 | config = hookenv.config() |
58 | @@ -98,6 +105,19 @@ def version(): | |||
59 | 98 | return version | 105 | return version |
60 | 99 | 106 | ||
61 | 100 | 107 | ||
62 | 108 | def clear_version_cache(): | ||
63 | 109 | unitdata.kv().set("postgresql.pg_version", None) | ||
64 | 110 | |||
65 | 111 | |||
66 | 112 | def lsclusters_version(): | ||
67 | 113 | if not os.path.exists("/usr/bin/pg_lsclusters"): | ||
68 | 114 | return None | ||
69 | 115 | results = subprocess.check_output(["pg_lsclusters", "--no-header"]).decode().splitlines() | ||
70 | 116 | if len(results) == 1: | ||
71 | 117 | return results[0].split()[0] | ||
72 | 118 | return None | ||
73 | 119 | |||
74 | 120 | |||
75 | 101 | def point_version(): | 121 | def point_version(): |
76 | 102 | """PostgreSQL version. major.minor.patch or major.patch, as a string.""" | 122 | """PostgreSQL version. major.minor.patch or major.patch, as a string.""" |
77 | 103 | output = subprocess.check_output([postgres_path(), "-V"], universal_newlines=True) | 123 | output = subprocess.check_output([postgres_path(), "-V"], universal_newlines=True) |
78 | @@ -473,10 +493,7 @@ def ensure_role(con, role): | |||
79 | 473 | def ensure_extensions(con, extensions): | 493 | def ensure_extensions(con, extensions): |
80 | 474 | """extensions is a list of (name, schema) tuples""" | 494 | """extensions is a list of (name, schema) tuples""" |
81 | 475 | cur = con.cursor() | 495 | cur = con.cursor() |
86 | 476 | cur.execute( | 496 | cur.execute("SELECT extname, nspname FROM pg_extension, pg_namespace WHERE pg_namespace.oid = extnamespace") |
83 | 477 | """SELECT extname,nspname FROM pg_extension,pg_namespace | ||
84 | 478 | WHERE pg_namespace.oid = extnamespace""" | ||
85 | 479 | ) | ||
87 | 480 | installed_extensions = frozenset((x[0], x[1]) for x in cur.fetchall()) | 497 | installed_extensions = frozenset((x[0], x[1]) for x in cur.fetchall()) |
88 | 481 | hookenv.log("ensure_extensions({}), have {}".format(extensions, installed_extensions), DEBUG) | 498 | hookenv.log("ensure_extensions({}), have {}".format(extensions, installed_extensions), DEBUG) |
89 | 482 | extensions_set = frozenset(set(extensions)) | 499 | extensions_set = frozenset(set(extensions)) |
90 | diff --git a/reactive/postgresql/service.py b/reactive/postgresql/service.py | |||
91 | index b7269b1..f5e1936 100644 | |||
92 | --- a/reactive/postgresql/service.py | |||
93 | +++ b/reactive/postgresql/service.py | |||
94 | @@ -45,6 +45,18 @@ def install(): | |||
95 | 45 | reactive.set_flag("postgresql.upgrade.systemd") | 45 | reactive.set_flag("postgresql.upgrade.systemd") |
96 | 46 | 46 | ||
97 | 47 | 47 | ||
98 | 48 | @hook("post-series-upgrade") | ||
99 | 49 | def post_series_upgrade(): | ||
100 | 50 | postgresql.clear_version_cache() # PG version upgrades should work on the master, but will break standbys | ||
101 | 51 | config = hookenv.config() | ||
102 | 52 | if config["pgdg"]: | ||
103 | 53 | add_pgdg_source() | ||
104 | 54 | if host.init_is_systemd(): | ||
105 | 55 | reactive.set_flag("postgresql.upgrade.systemd") | ||
106 | 56 | reactive.clear_flag("postgresql.cluster.support-scripts") | ||
107 | 57 | reactive.clear_flag("postgresql.cluster.configured") | ||
108 | 58 | |||
109 | 59 | |||
110 | 48 | @when("config.changed") | 60 | @when("config.changed") |
111 | 49 | def config_changed(): | 61 | def config_changed(): |
112 | 50 | reactive.clear_flag("postgresql.cluster.support-scripts") | 62 | reactive.clear_flag("postgresql.cluster.support-scripts") |
113 | @@ -143,12 +155,23 @@ def configure_sources(): | |||
114 | 143 | 155 | ||
115 | 144 | # Shortcut for the PGDG archive. | 156 | # Shortcut for the PGDG archive. |
116 | 145 | if config["pgdg"]: | 157 | if config["pgdg"]: |
120 | 146 | pgdg_url = "http://apt.postgresql.org/pub/repos/apt/" | 158 | add_pgdg_source() |
121 | 147 | pgdg_src = "deb {} {}-pgdg main".format(pgdg_url, helpers.distro_codename()) | 159 | |
122 | 148 | pgdg_key_path = os.path.join(hookenv.charm_dir(), "lib", "pgdg.key") | 160 | |
123 | 161 | def add_pgdg_source(): | ||
124 | 162 | pgdg_url = "http://apt.postgresql.org/pub/repos/apt/" | ||
125 | 163 | pgdg_src = "deb {} {}-pgdg main".format(pgdg_url, helpers.distro_codename()) | ||
126 | 164 | pgdg_key_path = os.path.join(hookenv.charm_dir(), "lib", "pgdg.key") | ||
127 | 165 | kv = unitdata.kv() | ||
128 | 166 | k = "postgresql.service.pgdg_src" | ||
129 | 167 | # Add the source if the key has changed, or if the src has | ||
130 | 168 | # changed such as a distro upgrade. Check key change first, | ||
131 | 169 | # or short circuiting will add the source twice. | ||
132 | 170 | if reactive.helpers.any_file_changed([pgdg_key_path]) or pgdg_src != kv.get(k): | ||
133 | 149 | with open(pgdg_key_path, "r") as f: | 171 | with open(pgdg_key_path, "r") as f: |
134 | 150 | hookenv.log("Adding PGDG archive") | 172 | hookenv.log("Adding PGDG archive") |
135 | 151 | apt.add_source(pgdg_src, f.read()) | 173 | apt.add_source(pgdg_src, f.read()) |
136 | 174 | kv.set(k, pgdg_src) | ||
137 | 152 | 175 | ||
138 | 153 | 176 | ||
139 | 154 | @when("postgresql.cluster.locale.set") | 177 | @when("postgresql.cluster.locale.set") |
140 | @@ -192,13 +215,13 @@ def create_cluster(): | |||
141 | 192 | def create_pg_ctl_conf(): | 215 | def create_pg_ctl_conf(): |
142 | 193 | contents = textwrap.dedent( | 216 | contents = textwrap.dedent( |
143 | 194 | """\ | 217 | """\ |
148 | 195 | # Managed by Juju | 218 | # Managed by Juju |
149 | 196 | # Automatic pg_ctl configuration | 219 | # Automatic pg_ctl configuration |
150 | 197 | # This configuration file contains cluster specific options to be passed to | 220 | # This configuration file contains cluster specific options to be passed to |
151 | 198 | # pg_ctl(1). | 221 | # pg_ctl(1). |
152 | 199 | 222 | ||
155 | 200 | pg_ctl_options = '-w -t 3600' | 223 | pg_ctl_options = '-w -t 3600' |
156 | 201 | """ | 224 | """ |
157 | 202 | ) | 225 | ) |
158 | 203 | helpers.write( | 226 | helpers.write( |
159 | 204 | postgresql.pg_ctl_conf_path(), | 227 | postgresql.pg_ctl_conf_path(), |
160 | diff --git a/reactive/postgresql/upgrade.py b/reactive/postgresql/upgrade.py | |||
161 | index 9379c60..d056319 100644 | |||
162 | --- a/reactive/postgresql/upgrade.py | |||
163 | +++ b/reactive/postgresql/upgrade.py | |||
164 | @@ -26,6 +26,7 @@ from reactive import workloadstatus | |||
165 | 26 | from reactive.postgresql import helpers | 26 | from reactive.postgresql import helpers |
166 | 27 | from reactive.postgresql import postgresql | 27 | from reactive.postgresql import postgresql |
167 | 28 | from reactive.postgresql import replication | 28 | from reactive.postgresql import replication |
168 | 29 | from reactive.postgresql import service | ||
169 | 29 | from reactive.postgresql import storage | 30 | from reactive.postgresql import storage |
170 | 30 | 31 | ||
171 | 31 | 32 | ||
172 | @@ -134,6 +135,11 @@ def upgrade_charm(): | |||
173 | 134 | reactive.clear_flag("postgresql.cluster.is_running") | 135 | reactive.clear_flag("postgresql.cluster.is_running") |
174 | 135 | postgresql.stop_pgctlcluster() | 136 | postgresql.stop_pgctlcluster() |
175 | 136 | 137 | ||
176 | 138 | # Update the PGDG source, in case the signing key has changed. | ||
177 | 139 | config = hookenv.config() | ||
178 | 140 | if config["pgdg"]: | ||
179 | 141 | service.add_pgdg_source() | ||
180 | 142 | |||
181 | 137 | 143 | ||
182 | 138 | def migrate_user(old_username, new_username, password, superuser=False): | 144 | def migrate_user(old_username, new_username, password, superuser=False): |
183 | 139 | if postgresql.is_primary(): | 145 | if postgresql.is_primary(): |
184 | diff --git a/requirements.txt b/requirements.txt | |||
185 | index 8aabe77..675a41a 100644 | |||
186 | --- a/requirements.txt | |||
187 | +++ b/requirements.txt | |||
188 | @@ -11,5 +11,6 @@ python-swiftclient | |||
189 | 11 | charms.reactive | 11 | charms.reactive |
190 | 12 | charmhelpers | 12 | charmhelpers |
191 | 13 | juju-deployer | 13 | juju-deployer |
194 | 14 | pip<8.1.2 | 14 | pip |
195 | 15 | websocket-client<=0.40.0 | 15 | websocket-client |
196 | 16 | tenacity | ||
197 | diff --git a/tox.ini b/tox.ini | |||
198 | index 71565ed..cb30fb6 100644 | |||
199 | --- a/tox.ini | |||
200 | +++ b/tox.ini | |||
201 | @@ -29,9 +29,10 @@ commands= | |||
202 | 29 | basepython=python3 | 29 | basepython=python3 |
203 | 30 | # pip install -I required to ensure scripts are created | 30 | # pip install -I required to ensure scripts are created |
204 | 31 | # even if packages are already installed on the system | 31 | # even if packages are already installed on the system |
208 | 32 | install_command=pip install -I {opts} {packages} | 32 | sitepackages=False |
209 | 33 | sitepackages=True | 33 | deps = |
210 | 34 | deps = -r{toxinidir}/requirements.txt | 34 | -r{toxinidir}/requirements.txt |
211 | 35 | psycopg2 | ||
212 | 35 | passenv=JUJU_* | 36 | passenv=JUJU_* |
213 | 36 | commands= | 37 | commands= |
214 | 37 | pytest {posargs:--verbose --tb=native unit_tests/} | 38 | pytest {posargs:--verbose --tb=native unit_tests/} |
215 | diff --git a/unit_tests/test_postgresql.py b/unit_tests/test_postgresql.py | |||
216 | index 5524745..95cf828 100644 | |||
217 | --- a/unit_tests/test_postgresql.py | |||
218 | +++ b/unit_tests/test_postgresql.py | |||
219 | @@ -26,47 +26,47 @@ sys.path.insert(1, ROOT) | |||
220 | 26 | sys.path.insert(2, os.path.join(ROOT, 'lib')) | 26 | sys.path.insert(2, os.path.join(ROOT, 'lib')) |
221 | 27 | sys.path.insert(3, os.path.join(ROOT, 'lib', 'testdeps')) | 27 | sys.path.insert(3, os.path.join(ROOT, 'lib', 'testdeps')) |
222 | 28 | 28 | ||
226 | 29 | from charmhelpers.core import hookenv | 29 | from charmhelpers.core import hookenv, host |
224 | 30 | from charmhelpers.core import unitdata | ||
225 | 31 | |||
227 | 32 | from reactive import workloadstatus | 30 | from reactive import workloadstatus |
230 | 33 | from reactive.postgresql import helpers | 31 | from reactive.postgresql import helpers, postgresql |
229 | 34 | from reactive.postgresql import postgresql | ||
231 | 35 | 32 | ||
232 | 36 | 33 | ||
233 | 37 | class TestPostgresql(unittest.TestCase): | 34 | class TestPostgresql(unittest.TestCase): |
234 | 35 | @patch.object(postgresql, 'lsclusters_version') | ||
235 | 38 | @patch.object(hookenv, 'config') | 36 | @patch.object(hookenv, 'config') |
236 | 39 | @patch.object(helpers, 'distro_codename') | 37 | @patch.object(helpers, 'distro_codename') |
241 | 40 | def test_version(self, codename, config): | 38 | def test_version(self, codename, config, lsclusters_version): |
242 | 41 | 39 | # Installed version | |
243 | 42 | def clear_cache(): | 40 | lsclusters_version.return_value = '9.5' |
244 | 43 | unitdata.kv().unset('postgresql.pg_version') | 41 | postgresql.clear_version_cache() |
245 | 42 | self.assertEqual(postgresql.version(), '9.5') | ||
246 | 43 | lsclusters_version.return_value = None | ||
247 | 44 | 44 | ||
248 | 45 | # Explicit version in config. | 45 | # Explicit version in config. |
249 | 46 | config.return_value = {'version': '23'} | 46 | config.return_value = {'version': '23'} |
251 | 47 | clear_cache() | 47 | postgresql.clear_version_cache() |
252 | 48 | self.assertEqual(postgresql.version(), '23') | 48 | self.assertEqual(postgresql.version(), '23') |
253 | 49 | 49 | ||
254 | 50 | config.return_value = {'version': ''} | 50 | config.return_value = {'version': ''} |
255 | 51 | 51 | ||
256 | 52 | # Trusty default | 52 | # Trusty default |
257 | 53 | codename.return_value = 'trusty' | 53 | codename.return_value = 'trusty' |
259 | 54 | clear_cache() | 54 | postgresql.clear_version_cache() |
260 | 55 | self.assertEqual(postgresql.version(), '9.3') | 55 | self.assertEqual(postgresql.version(), '9.3') |
261 | 56 | 56 | ||
262 | 57 | # Xenial default | 57 | # Xenial default |
263 | 58 | codename.return_value = 'xenial' | 58 | codename.return_value = 'xenial' |
265 | 59 | clear_cache() | 59 | postgresql.clear_version_cache() |
266 | 60 | self.assertEqual(postgresql.version(), '9.5') | 60 | self.assertEqual(postgresql.version(), '9.5') |
267 | 61 | 61 | ||
268 | 62 | # Bionic default | 62 | # Bionic default |
269 | 63 | codename.return_value = 'bionic' | 63 | codename.return_value = 'bionic' |
271 | 64 | clear_cache() | 64 | postgresql.clear_version_cache() |
272 | 65 | self.assertEqual(postgresql.version(), '10') | 65 | self.assertEqual(postgresql.version(), '10') |
273 | 66 | 66 | ||
274 | 67 | # No other fallbacks, yet. | 67 | # No other fallbacks, yet. |
275 | 68 | codename.return_value = 'whatever' | 68 | codename.return_value = 'whatever' |
277 | 69 | clear_cache() | 69 | postgresql.clear_version_cache() |
278 | 70 | with self.assertRaises(NotImplementedError): | 70 | with self.assertRaises(NotImplementedError): |
279 | 71 | postgresql.version() | 71 | postgresql.version() |
280 | 72 | 72 | ||
281 | @@ -251,8 +251,11 @@ class TestPostgresql(unittest.TestCase): | |||
282 | 251 | is_secondary.return_value = False | 251 | is_secondary.return_value = False |
283 | 252 | self.assertTrue(postgresql.is_primary()) | 252 | self.assertTrue(postgresql.is_primary()) |
284 | 253 | 253 | ||
285 | 254 | @patch.object(postgresql, 'has_version') | ||
286 | 255 | @patch.object(hookenv, 'config') | ||
287 | 254 | @patch.object(postgresql, 'recovery_conf_path') | 256 | @patch.object(postgresql, 'recovery_conf_path') |
289 | 255 | def test_is_secondary(self, recovery_path): | 257 | def test_is_secondary(self, recovery_path, config, has_version): |
290 | 258 | has_version.return_value = False | ||
291 | 256 | # if recovery.conf exists, we are a secondary. | 259 | # if recovery.conf exists, we are a secondary. |
292 | 257 | with tempfile.NamedTemporaryFile() as f: | 260 | with tempfile.NamedTemporaryFile() as f: |
293 | 258 | recovery_path.return_value = f.name | 261 | recovery_path.return_value = f.name |
294 | @@ -317,7 +320,7 @@ class TestPostgresql(unittest.TestCase): | |||
295 | 317 | check_call.assert_called_once_with(['pg_createcluster', | 320 | check_call.assert_called_once_with(['pg_createcluster', |
296 | 318 | '-e', sentinel.encoding, | 321 | '-e', sentinel.encoding, |
297 | 319 | '--locale', sentinel.locale, | 322 | '--locale', sentinel.locale, |
299 | 320 | '9.2', 'main'], | 323 | '9.2', 'main', '--', '--data-checksums'], |
300 | 321 | universal_newlines=True) | 324 | universal_newlines=True) |
301 | 322 | 325 | ||
302 | 323 | @patch('subprocess.check_call') | 326 | @patch('subprocess.check_call') |
303 | @@ -345,7 +348,7 @@ class TestPostgresql(unittest.TestCase): | |||
304 | 345 | cur.execute.assert_has_calls([ | 348 | cur.execute.assert_has_calls([ |
305 | 346 | call('SELECT datname FROM pg_database WHERE datname=%s', | 349 | call('SELECT datname FROM pg_database WHERE datname=%s', |
306 | 347 | ('hello',)), | 350 | ('hello',)), |
308 | 348 | call('CREATE DATABASE %s', (ANY,))]) | 351 | call('CREATE DATABASE %s OWNER %s', (ANY, ANY))]) |
309 | 349 | # The database name in that last call was correctly quoted. | 352 | # The database name in that last call was correctly quoted. |
310 | 350 | quoted_dbname = cur.execute.call_args[0][1][0] | 353 | quoted_dbname = cur.execute.call_args[0][1][0] |
311 | 351 | self.assertIsInstance(quoted_dbname, postgresql.AsIs) | 354 | self.assertIsInstance(quoted_dbname, postgresql.AsIs) |
312 | @@ -475,14 +478,16 @@ class TestPostgresql(unittest.TestCase): | |||
313 | 475 | 478 | ||
314 | 476 | pgidentifier.side_effect = lambda d: 'q_{}'.format(d) | 479 | pgidentifier.side_effect = lambda d: 'q_{}'.format(d) |
315 | 477 | 480 | ||
318 | 478 | existing_extensions = set(['extA', 'extB']) | 481 | existing_extensions = set([('extA', 'public'), ('extB', 'public')]) |
319 | 479 | wanted_extensions = set(['extB', 'extC']) | 482 | wanted_extensions = set([('extB', 'public'), ('extC', 'custom')]) |
320 | 480 | 483 | ||
322 | 481 | cur.fetchall.return_value = [[x] for x in existing_extensions] | 484 | cur.fetchall.return_value = [x for x in existing_extensions] |
323 | 482 | postgresql.ensure_extensions(con, wanted_extensions) | 485 | postgresql.ensure_extensions(con, wanted_extensions) |
324 | 483 | cur.execute.assert_has_calls([ | 486 | cur.execute.assert_has_calls([ |
327 | 484 | call('SELECT extname FROM pg_extension'), | 487 | call('SELECT extname, nspname FROM pg_extension, pg_namespace WHERE pg_namespace.oid = extnamespace'), |
328 | 485 | call('CREATE EXTENSION %s', ('q_extC',))]) | 488 | call('CREATE SCHEMA IF NOT EXISTS %s', ('q_custom',)), |
329 | 489 | call('GRANT USAGE ON SCHEMA %s TO PUBLIC', ('q_custom',)), | ||
330 | 490 | call('CREATE EXTENSION %s WITH SCHEMA %s', ('q_extC','q_custom'))]) | ||
331 | 486 | 491 | ||
332 | 487 | def test_addr_to_range(self): | 492 | def test_addr_to_range(self): |
333 | 488 | eggs = [('hostname', 'hostname'), | 493 | eggs = [('hostname', 'hostname'), |
334 | @@ -532,35 +537,18 @@ class TestPostgresql(unittest.TestCase): | |||
335 | 532 | 537 | ||
336 | 533 | @patch.object(postgresql, 'emit_pg_log') | 538 | @patch.object(postgresql, 'emit_pg_log') |
337 | 534 | @patch.object(workloadstatus, 'status_set') | 539 | @patch.object(workloadstatus, 'status_set') |
339 | 535 | @patch('subprocess.check_call') | 540 | @patch.object(host, 'service_start') |
340 | 536 | @patch.object(postgresql, 'version') | 541 | @patch.object(postgresql, 'version') |
342 | 537 | def test_start(self, version, check_call, status_set, emit_pg_log): | 542 | def test_start(self, version, service_start, status_set, emit_pg_log): |
343 | 538 | version.return_value = '9.9' | 543 | version.return_value = '9.9' |
344 | 539 | 544 | ||
345 | 540 | # When it works, it works. | 545 | # When it works, it works. |
346 | 541 | postgresql.start() | 546 | postgresql.start() |
354 | 542 | # Both -w and -t options are required to wait for startup. | 547 | service_start.assert_called_once_with('postgresql@9.9-main') |
348 | 543 | # We wait a long time, as startup might take a long time. | ||
349 | 544 | # Maybe we should wait a lot longer. | ||
350 | 545 | check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', | ||
351 | 546 | 'start', '--', '-w', | ||
352 | 547 | '-t', '86400'], | ||
353 | 548 | universal_newlines=True) | ||
355 | 549 | self.assertFalse(emit_pg_log.called) | 548 | self.assertFalse(emit_pg_log.called) |
356 | 550 | 549 | ||
370 | 551 | # If it is already running, pg_ctlcluster returns code 2. | 550 | # Start failure we block, and terminate whatever hook is running. |
371 | 552 | # We block, and terminate whatever hook is running. | 551 | service_start.return_value = False |
359 | 553 | check_call.side_effect = subprocess.CalledProcessError(2, 'whoops') | ||
360 | 554 | check_call.reset_mock() | ||
361 | 555 | postgresql.start() | ||
362 | 556 | check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', | ||
363 | 557 | 'start', '--', '-w', | ||
364 | 558 | '-t', '86400'], | ||
365 | 559 | universal_newlines=True) | ||
366 | 560 | |||
367 | 561 | # Other failures block the unit. Perhaps it is just taking too | ||
368 | 562 | # perform recovery after a power outage. | ||
369 | 563 | check_call.side_effect = subprocess.CalledProcessError(42, 'whoops') | ||
372 | 564 | with self.assertRaises(SystemExit) as x: | 552 | with self.assertRaises(SystemExit) as x: |
373 | 565 | postgresql.start() | 553 | postgresql.start() |
374 | 566 | status_set.assert_called_once_with('blocked', ANY) # Set blocked. | 554 | status_set.assert_called_once_with('blocked', ANY) # Set blocked. |
375 | @@ -569,59 +557,29 @@ class TestPostgresql(unittest.TestCase): | |||
376 | 569 | 557 | ||
377 | 570 | @patch.object(hookenv, 'log') | 558 | @patch.object(hookenv, 'log') |
378 | 571 | @patch.object(workloadstatus, 'status_set') | 559 | @patch.object(workloadstatus, 'status_set') |
380 | 572 | @patch('subprocess.check_call') | 560 | @patch.object(host, 'service_stop') |
381 | 573 | @patch.object(postgresql, 'version') | 561 | @patch.object(postgresql, 'version') |
383 | 574 | def test_stop(self, version, check_call, status_set, log): | 562 | def test_stop(self, version, service_stop, status_set, log): |
384 | 575 | version.return_value = '9.9' | 563 | version.return_value = '9.9' |
385 | 576 | 564 | ||
386 | 577 | # Normal shutdown shuts down. | 565 | # Normal shutdown shuts down. |
387 | 566 | service_stop.return_value = True | ||
388 | 578 | postgresql.stop() | 567 | postgresql.stop() |
407 | 579 | # -t option is required to wait for shutdown to complete. -w not | 568 | service_stop.assert_called_once_with('postgresql@9.9-main') |
390 | 580 | # required unlike 'start', but lets be explicit. | ||
391 | 581 | check_call.assert_called_once_with(['pg_ctlcluster', | ||
392 | 582 | '--mode', 'fast', '9.9', 'main', | ||
393 | 583 | 'stop', '--', '-w', '-t', '300'], | ||
394 | 584 | universal_newlines=True) | ||
395 | 585 | |||
396 | 586 | # If the server is not running, pg_ctlcluster(1) signals this with | ||
397 | 587 | # returncode 2. | ||
398 | 588 | check_call.side_effect = subprocess.CalledProcessError(2, 'whoops') | ||
399 | 589 | check_call.reset_mock() | ||
400 | 590 | postgresql.stop() | ||
401 | 591 | # -t option is required to wait for shutdown to complete. -w not | ||
402 | 592 | # required unlike 'start', but lets be explicit. | ||
403 | 593 | check_call.assert_called_once_with(['pg_ctlcluster', | ||
404 | 594 | '--mode', 'fast', '9.9', 'main', | ||
405 | 595 | 'stop', '--', '-w', '-t', '300'], | ||
406 | 596 | universal_newlines=True) | ||
408 | 597 | 569 | ||
424 | 598 | # If 'fast' shutdown fails, we retry with an 'immediate' shutdown | 570 | # Failed shutdown blocks and terminates |
425 | 599 | check_call.side_effect = iter([subprocess.CalledProcessError(42, 'x'), | 571 | service_stop.return_value = False |
411 | 600 | None]) | ||
412 | 601 | check_call.reset_mock() | ||
413 | 602 | postgresql.stop() | ||
414 | 603 | check_call.assert_has_calls([ | ||
415 | 604 | call(['pg_ctlcluster', '--mode', 'fast', '9.9', 'main', | ||
416 | 605 | 'stop', '--', '-w', '-t', '300'], | ||
417 | 606 | universal_newlines=True), | ||
418 | 607 | call(['pg_ctlcluster', '--mode', 'immediate', '9.9', 'main', | ||
419 | 608 | 'stop', '--', '-w', '-t', '300'], | ||
420 | 609 | universal_newlines=True)]) | ||
421 | 610 | |||
422 | 611 | # If both fail, we block the unit. | ||
423 | 612 | check_call.side_effect = subprocess.CalledProcessError(42, 'x') | ||
426 | 613 | with self.assertRaises(SystemExit) as x: | 572 | with self.assertRaises(SystemExit) as x: |
427 | 614 | postgresql.stop() | 573 | postgresql.stop() |
428 | 615 | status_set.assert_called_once_with('blocked', ANY) | 574 | status_set.assert_called_once_with('blocked', ANY) |
429 | 616 | self.assertEqual(x.exception.code, 0) # Exit cleanly | 575 | self.assertEqual(x.exception.code, 0) # Exit cleanly |
430 | 617 | 576 | ||
432 | 618 | @patch('subprocess.check_call') | 577 | @patch.object(host, 'service_reload') |
433 | 619 | @patch.object(postgresql, 'version') | 578 | @patch.object(postgresql, 'version') |
435 | 620 | def test_reload_config(self, version, check_call): | 579 | def test_reload_config(self, version, service_reload): |
436 | 621 | version.return_value = '9.9' | 580 | version.return_value = '9.9' |
437 | 622 | postgresql.reload_config() | 581 | postgresql.reload_config() |
440 | 623 | check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', | 582 | service_reload.assert_called_once_with('postgresql@9.9-main') |
439 | 624 | 'reload']) | ||
441 | 625 | 583 | ||
442 | 626 | def test_parse_config(self): | 584 | def test_parse_config(self): |
443 | 627 | valid = [(r'# A comment', dict()), | 585 | valid = [(r'# A comment', dict()), |
444 | @@ -653,7 +611,7 @@ class TestPostgresql(unittest.TestCase): | |||
445 | 653 | 611 | ||
446 | 654 | with self.assertRaises(SyntaxError) as x: | 612 | with self.assertRaises(SyntaxError) as x: |
447 | 655 | postgresql.parse_config("=") | 613 | postgresql.parse_config("=") |
449 | 656 | self.assertEqual(str(x.exception), 'Missing key (line 1)') | 614 | self.assertEqual(str(x.exception), "Missing key '=' (line 1)") |
450 | 657 | self.assertEqual(x.exception.lineno, 1) | 615 | self.assertEqual(x.exception.lineno, 1) |
451 | 658 | self.assertEqual(x.exception.text, "=") | 616 | self.assertEqual(x.exception.text, "=") |
452 | 659 | 617 | ||
453 | @@ -665,19 +623,19 @@ class TestPostgresql(unittest.TestCase): | |||
454 | 665 | 623 | ||
455 | 666 | with self.assertRaises(SyntaxError) as x: | 624 | with self.assertRaises(SyntaxError) as x: |
456 | 667 | postgresql.parse_config("key='unterminated") | 625 | postgresql.parse_config("key='unterminated") |
458 | 668 | self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') | 626 | self.assertEqual(str(x.exception), r'''Badly quoted value "'unterminated" (line 1)''') |
459 | 669 | 627 | ||
460 | 670 | with self.assertRaises(SyntaxError) as x: | 628 | with self.assertRaises(SyntaxError) as x: |
461 | 671 | postgresql.parse_config("key='unterminated 2 # comment") | 629 | postgresql.parse_config("key='unterminated 2 # comment") |
463 | 672 | self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') | 630 | self.assertEqual(str(x.exception), r'''Badly quoted value "'unterminated 2" (line 1)''') |
464 | 673 | 631 | ||
465 | 674 | with self.assertRaises(SyntaxError) as x: | 632 | with self.assertRaises(SyntaxError) as x: |
466 | 675 | postgresql.parse_config("key='unte''''") | 633 | postgresql.parse_config("key='unte''''") |
468 | 676 | self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') | 634 | self.assertEqual(str(x.exception), r"""Badly quoted value "'unte''''" (line 1)""") |
469 | 677 | 635 | ||
470 | 678 | with self.assertRaises(SyntaxError) as x: | 636 | with self.assertRaises(SyntaxError) as x: |
471 | 679 | postgresql.parse_config(r"key='\'") | 637 | postgresql.parse_config(r"key='\'") |
473 | 680 | self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') | 638 | self.assertEqual(str(x.exception), r'''Badly quoted value "'\\'" (line 1)''') |
474 | 681 | 639 | ||
475 | 682 | def test_convert_unit(self): | 640 | def test_convert_unit(self): |
476 | 683 | c = postgresql.convert_unit | 641 | c = postgresql.convert_unit |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.