Merge ~vultaire/charm-graylog:protected-upgrades into charm-graylog:master
- Git
- lp:~vultaire/charm-graylog
- protected-upgrades
- Merge into master
Status: | Merged |
---|---|
Approved by: | Alvaro Uria |
Approved revision: | 24d8b9d699af51b9c5816ddf052ef0cf9c2721ac |
Merged at revision: | 330ea1929b1d32109643abc0c2291786ce6dad78 |
Proposed branch: | ~vultaire/charm-graylog:protected-upgrades |
Merge into: | charm-graylog:master |
Diff against target: |
919 lines (+469/-106) 15 files modified
Makefile (+2/-2) actions.yaml (+11/-2) actions/actions.py (+30/-13) actions/do-upgrade (+1/-0) lib/charms/layer/graylog/constants.py (+11/-0) lib/charms/layer/graylog/snap_change.py (+95/-0) lib/charms/layer/graylog/utils.py (+6/-2) reactive/graylog.py (+47/-41) tests/requirements.txt (+1/-0) tests/test_graylog_charm.py (+9/-3) tests/test_graylog_upgrade.py (+159/-0) tests/tests.yaml (+15/-10) unit_tests/test_graylog.py (+62/-21) unit_tests/test_lib.py (+8/-8) unit_tests/test_logextract.py (+12/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alvaro Uria (community) | Approve | ||
Xav Paice (community) | Approve | ||
Drew Freiberger (community) | Approve | ||
Joe Guo (community) | Needs Fixing | ||
Peter Sabaini (community) | Needs Information | ||
Review via email: mp+385686@code.launchpad.net |
Commit message
Implemented "protected upgrades" support
The intent is to provide "checkpoints" around major version upgrades
of the Graylog snap:
* Major upgrade (e.g. track increased from 2 to 3): block and guide
user on next steps.
* Major downgrade (e.g. track decreased from 3 to 2): disallow; ask
user to revert channel change in charm config.
* Special meaning tracks (i.e. latest): disallow; ask user to revert.
* No track change (e.g. 3/stable to 3/candidate): allow.
Description of the change
Joe Guo (guoqiao) wrote : | # |
A few inline comments, otherwise looks good to me.
Drew Freiberger (afreiberger) wrote : | # |
mainly echoing joe's comments inline, Love the constants, feel they should be named in imports rather than wildcarded so it passes lint.
Paul Goins (vultaire) wrote : | # |
Replied to comments.
Paul Goins (vultaire) : | # |
Paul Goins (vultaire) : | # |
Paul Goins (vultaire) wrote : | # |
OK, I think I've addressed most of the comments here.
I've intentionally skipped running black on the changed sections as that is extra work with very little gain right now, as we're planning to run black over the entire charm afterwards anyway.
Ready for re-review.
Drew Freiberger (afreiberger) wrote : | # |
Thanks for the cleanup, looks good to me.
Alvaro Uria (aluria) wrote : | # |
I've found a few issues that could be fixed as follows:
https:/
In the Makefile, /tmp/graylog-built should be used or this error will happen:
"""
2020-08-03 12:19:46 [INFO] Deploying bundle './tests/
2020-08-03 12:19:46 [INFO] Rendered template '<Template 'local-
2020-08-03 12:19:46 [INFO] Deploying overlay '/tmp/tmp77_
2020-08-03 12:19:46 [INFO] ERROR cannot deploy bundle: the provided bundle has the following errors:
2020-08-03 12:19:46 [INFO] charm path in application "graylog" does not exist: /tmp/graylog-
"""
I added tenacity to retry the search for results, as a slow test env may return 0 findings. I took the following as an example:
https:/
the hooks.hook decorator needs to be called, since the decorated function is passed in a wrapped function (because hook method can take arguments). Since the hook name is taken from the function name in actions/*.py, the func name needs to be renamed.
On line 153, the do-upgrade action will stop the graylog service, and then remove charm states. It seems the unit will wait until update-status (or any other hook) is triggered to reconfigure graylog and start the service again. Or will removing a state trigger the reconfiguration immediately?
Linting failed because "sysdeps" calls "sudo". Since the Makefiles will be changed across all the LMA stack, I only ran "make functional" which seems to work as expected (tests are still running, but the first bundle passed fine).
Paul Goins (vultaire) wrote : | # |
Replied to some of the comments. Will think this over.
Alvaro Uria (aluria) wrote : | # |
Func tests fail because "charm build" happens in ../graylog-built when it should happen in /tmp/graylog-built (otherwise, overlay.yaml file won't find the graylog code).
Paul Goins (vultaire) wrote : | # |
Just ran a full set of tests (and got lucky that elasticsearch didn't have any of its random failures). Worked fine.
Am preparing to run another set of tests with CHARM_*_DIR unset in the environment, so as to exercise the /tmp/graylog-built path. I also cleared out the charm build directory just in case, and ensured that neither /tmp/graylog-built nor ../graylog-built exist.
Note: I am using global-level proxy settings in my MODEL_SETTINGS in order for this to run okay as the current cs:elasticsearch requires this.
Paul Goins (vultaire) wrote : | # |
Re-ran functional tests successfully. Had one run which timed out, admittedly, and with the current issues of cs:elasticsearch I can't say things are reliable, but I believe things are good enough to let us wrap this MR up, if it passes review and if someone else can verify the tests passing.
Xav Paice (xavpaice) wrote : | # |
`make lint` fails with several errors, but I think that should be left for the lint/black process. The current iteration of the makefile refuses to run unit tests because of this, but tox -e unit results in good tests.
The functional tests are failing:
2020-08-10 11:17:40 [INFO] FAIL
2020-08-10 11:17:40 [INFO] =======
2020-08-10 11:17:40 [INFO] ERROR: test_02_
2020-08-10 11:17:40 [INFO] -------
2020-08-10 11:17:40 [INFO] Traceback (most recent call last):
2020-08-10 11:17:40 [INFO] File "./tests/
2020-08-10 11:17:40 [INFO] self._block_until(
2020-08-10 11:17:40 [INFO] File "./tests/
2020-08-10 11:17:40 [INFO] model.block_
2020-08-10 11:17:40 [INFO] File "/home/
2020-08-10 11:17:40 [INFO] return run(_run_it())
2020-08-10 11:17:40 [INFO] File "/home/
2020-08-10 11:17:40 [INFO] return task.result()
2020-08-10 11:17:40 [INFO] File "/home/
2020-08-10 11:17:40 [INFO] return await f(*args, **kwargs)
2020-08-10 11:17:40 [INFO] File "/home/
2020-08-10 11:17:40 [INFO] await async_block_
2020-08-10 11:17:40 [INFO] File "/home/
2020-08-10 11:17:40 [INFO] await asyncio.
2020-08-10 11:17:40 [INFO] File "/usr/lib/
2020-08-10 11:17:40 [INFO] raise exceptions.
2020-08-10 11:17:40 [INFO] asyncio.
2020-08-10 11:17:40 [INFO] =======
2020-08-10 11:17:40 [INFO] ERROR: test_03_
2020-08-10 11:17:40 [INFO] -------
2020-08-10 11:17:40 [INFO] Traceback (most recent call last):
2020-08-10 11:17:40 [INFO] File "./tests/
2020-08-10 11:17:40 [INFO] self._block_until(
2020-08-10 11:17:40 [INFO] File "./tests/
2020-08-10 11:17:40 [INFO] model.block_
2020-08-10 11:17:40 [INFO] File "/home/
Xav Paice (xavpaice) wrote : | # |
Re-ran the functests, and this time they all ran through fine (including the previously failed tests). I guess it could be environmental on my test platform - but will ask for another review also.
Alvaro Uria (aluria) wrote : | # |
As mentioned in previous weeks, Lint and unit tests fail but will leave that after the Makefile and tox.ini files are updated. Unit tests failed because they require sudo privileges, which shouldn't be needed (required packages should live in a tox env).
WRT functests, they worked until the last one, focal-graylog3, which failed on test_10. Juju status looked fine so I guess retries should happen every 10s (as it is configured) but 6 times (instead of the current 3 times). When I re-ran the focal-graylog3 func test, it succeeded without issues.
I'll give this +1 so the other reworks can be started (makefile, blacken, extra lint).
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index ca1b305..d9b5017 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -1,8 +1,8 @@ |
6 | # Set charm build directory via CHARM_BUILD_DIR. |
7 | # JUJU_REPOSITORY is here purely for backwards compatibility. |
8 | -# If not specified, CHARM_BUILD_DIR will default to ../graylog-built/builds |
9 | +# If not specified, CHARM_BUILD_DIR will default to /tmp/graylog-built/builds |
10 | |
11 | -JUJU_REPOSITORY:=$(if $(JUJU_REPOSITORY),$(JUJU_REPOSITORY),../graylog-built) |
12 | +JUJU_REPOSITORY:=$(if $(JUJU_REPOSITORY),$(JUJU_REPOSITORY),/tmp/graylog-built) |
13 | CHARM_BUILD_DIR:=$(if $(CHARM_BUILD_DIR),$(CHARM_BUILD_DIR),$(JUJU_REPOSITORY)/builds) |
14 | BUILTCHARMDIR:=$(CHARM_BUILD_DIR)/graylog |
15 | |
16 | diff --git a/actions.yaml b/actions.yaml |
17 | index c8d6f78..90d3b74 100644 |
18 | --- a/actions.yaml |
19 | +++ b/actions.yaml |
20 | @@ -11,5 +11,14 @@ set-admin-password: |
21 | - password |
22 | ignore-indexer-failures: |
23 | description: | |
24 | - From NRPE check viewpoint, ignore all indexer failures that have occurred before the action is run. |
25 | - Future indexer failure occurrences will still be checked. |
26 | + From NRPE check viewpoint, ignore all indexer failures that have occurred before |
27 | + the action is run. Future indexer failure occurrences will still be checked. |
28 | +do-upgrade: |
29 | + description: | |
30 | + Perform a snap upgrade. This should be run after having already changed the |
31 | + configured channel and having followed the manual upgrade instructions documented |
32 | + in the charm's README.md This action will perform the final automatic steps |
33 | + to complete the upgrade. |
34 | + |
35 | + If the channel config option has not been changed appropriately, this action will |
36 | + not proceed. |
37 | diff --git a/actions/actions.py b/actions/actions.py |
38 | index 5b9808d..3564741 100755 |
39 | --- a/actions/actions.py |
40 | +++ b/actions/actions.py |
41 | @@ -18,6 +18,14 @@ from charms import reactive # noqa E402 |
42 | from charmhelpers.core import hookenv, host, unitdata # noqa E402 |
43 | |
44 | from charms.layer.graylog.api import get_ignore_indexer_failures_file # noqa E402 |
45 | +from charms.layer.graylog.snap_change import ( |
46 | + ChannelChangeStatus, |
47 | + get_channel_change_status, |
48 | + perform_snap_channel_change, |
49 | +) |
50 | + |
51 | + |
52 | +hooks = hookenv.Hooks() |
53 | |
54 | |
55 | def reactive_remove_state(state): |
56 | @@ -25,7 +33,8 @@ def reactive_remove_state(state): |
57 | reactive.main() |
58 | |
59 | |
60 | -def get_admin_passwd(): |
61 | +@hooks.hook('show-admin-password') |
62 | +def show_admin_password(): |
63 | db = unitdata.kv() |
64 | admin_password = db.get('admin_password') |
65 | if not admin_password: |
66 | @@ -35,7 +44,9 @@ def get_admin_passwd(): |
67 | hookenv.action_set({'admin-password': admin_password}) |
68 | |
69 | |
70 | -def set_admin_passwd(admin_password=None): |
71 | +@hooks.hook('set-admin-password') |
72 | +def set_admin_password(): |
73 | + admin_password = hookenv.action_get()['password'] |
74 | db = unitdata.kv() |
75 | stored_password = db.get('admin_password') |
76 | if admin_password == stored_password: |
77 | @@ -52,28 +63,34 @@ def set_admin_passwd(admin_password=None): |
78 | reactive_remove_state('graylog.configured') |
79 | |
80 | |
81 | +@hooks.hook('ignore-indexer-failures') |
82 | def ignore_indexer_failures(): |
83 | with open(get_ignore_indexer_failures_file(), 'w') as f: |
84 | f.write(datetime.datetime.utcnow().isoformat()) |
85 | |
86 | |
87 | +@hooks.hook('do-upgrade') |
88 | +def do_upgrade(): |
89 | + status = get_channel_change_status() |
90 | + if status == ChannelChangeStatus.UPGRADE_REQUESTED: |
91 | + new_channel = hookenv.config("channel") |
92 | + perform_snap_channel_change(new_channel) |
93 | + # Let reactive hooks react to the above. |
94 | + reactive.main() |
95 | + else: |
96 | + hookenv.action_fail( |
97 | + "No action taken; app is not blocked by a pending channel upgrade" |
98 | + ) |
99 | + |
100 | + |
101 | def main(argv): |
102 | - action = os.path.basename(argv[0]) |
103 | - params = hookenv.action_get() |
104 | try: |
105 | - if action == 'show-admin-password': |
106 | - get_admin_passwd() |
107 | - elif action == 'set-admin-password': |
108 | - set_admin_passwd(params['password']) |
109 | - elif action == 'ignore-indexer-failures': |
110 | - ignore_indexer_failures() |
111 | - else: |
112 | - hookenv.action_fail('Action {} not implemented'.format(action)) |
113 | + hooks.execute(argv) |
114 | except Exception: |
115 | hookenv.action_fail('Unhandled exception') |
116 | tb = traceback.format_exc() |
117 | hookenv.action_set(dict(traceback=tb)) |
118 | - hookenv.log('Unhandled exception in action {}'.format(action)) |
119 | + hookenv.log('Unhandled exception while running: {!r}'.format(argv)) |
120 | print(tb) |
121 | |
122 | |
123 | diff --git a/actions/do-upgrade b/actions/do-upgrade |
124 | new file mode 120000 |
125 | index 0000000..405a394 |
126 | --- /dev/null |
127 | +++ b/actions/do-upgrade |
128 | @@ -0,0 +1 @@ |
129 | +actions.py |
130 | \ No newline at end of file |
131 | diff --git a/lib/charms/layer/graylog/constants.py b/lib/charms/layer/graylog/constants.py |
132 | new file mode 100644 |
133 | index 0000000..2ebea47 |
134 | --- /dev/null |
135 | +++ b/lib/charms/layer/graylog/constants.py |
136 | @@ -0,0 +1,11 @@ |
137 | +SNAP_NAME = "graylog" |
138 | +CONF_FILE = '/var/snap/graylog/common/server.conf' |
139 | +SNAP_CONF_FILE = '/snap/graylog/current/etc/graylog/server/server.conf' |
140 | +# /snap/graylog is a read-only squashfs so we use the initial settings |
141 | +# and write out an override to SERVER_DEFAULT_CONF_FILE |
142 | +SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server' |
143 | +SERVER_DEFAULT_CONF_FILE = '/var/snap/graylog/current/default-graylog-server' |
144 | +ELASTICSEARCH_DISCOVERY_PORT = '9300' |
145 | +SERVICE_NAME = 'snap.graylog.graylog' |
146 | +DEFAULT_REST_API_TIMEOUT = 120 |
147 | +NAGIOS_USERNAME = 'nagios' |
148 | diff --git a/lib/charms/layer/graylog/snap_change.py b/lib/charms/layer/graylog/snap_change.py |
149 | new file mode 100644 |
150 | index 0000000..e8ed216 |
151 | --- /dev/null |
152 | +++ b/lib/charms/layer/graylog/snap_change.py |
153 | @@ -0,0 +1,95 @@ |
154 | +import enum |
155 | +import os |
156 | +import shutil |
157 | + |
158 | +from charms.layer.graylog.constants import ( |
159 | + SNAP_NAME, |
160 | + CONF_FILE, |
161 | + SNAP_CONF_FILE, |
162 | + SERVICE_NAME, |
163 | +) |
164 | + |
165 | +from charmhelpers.core import host, hookenv, unitdata |
166 | +from charms.reactive import remove_state |
167 | +from charms.reactive.helpers import data_changed |
168 | + |
169 | +# Allow mocking of other layers during unit tests |
170 | +try: |
171 | + from charms.layer import snap |
172 | +except ImportError: |
173 | + snap = None |
174 | + |
175 | + |
176 | +class ChannelChangeStatus(enum.Enum): |
177 | + NO_CHANGE = 1 |
178 | + INVALID = 2 |
179 | + UPGRADE_REQUESTED = 3 |
180 | + DOWNGRADE_REQUESTED = 4 |
181 | + LATEST_REQUESTED = 5 |
182 | + MINOR_CHANGE = 6 |
183 | + |
184 | + |
185 | +def get_channel_change_status(): |
186 | + """Given the current and requested channel, computes the channel change status. |
187 | + |
188 | + Specifically, this refers to whether there is any meaningful delta between the |
189 | + installed snap's channel and the requested channel - specifically, the track/risk, |
190 | + excluding the optional branch. |
191 | + |
192 | + Returns a value from the ChannelChangeStatus enum. |
193 | + |
194 | + """ |
195 | + new_channel = hookenv.config("channel") |
196 | + cur_channel = snap.get_installed_channel(SNAP_NAME) |
197 | + if (not new_channel) or new_channel == cur_channel: |
198 | + return ChannelChangeStatus.NO_CHANGE |
199 | + elif not _is_channel_valid(new_channel): |
200 | + return ChannelChangeStatus.INVALID |
201 | + else: |
202 | + cur_track = cur_channel.split("/")[0] |
203 | + new_track = new_channel.split("/")[0] |
204 | + if new_track == "latest": |
205 | + return ChannelChangeStatus.LATEST_REQUESTED |
206 | + else: |
207 | + cur_track = int(cur_track) |
208 | + new_track = int(new_track) |
209 | + if new_track > cur_track: |
210 | + return ChannelChangeStatus.UPGRADE_REQUESTED |
211 | + elif new_track < cur_track: |
212 | + return ChannelChangeStatus.DOWNGRADE_REQUESTED |
213 | + else: |
214 | + return ChannelChangeStatus.MINOR_CHANGE |
215 | + |
216 | + |
217 | +def _is_channel_valid(channel): |
218 | + try: |
219 | + track, risk = channel.split('/')[:2] |
220 | + except ValueError: |
221 | + return False |
222 | + # NOTE: While we won't allow changing to the "latest" channel, it is a valid |
223 | + # channel for the snap. We'll reject it elsewhere. |
224 | + snap_tracks = ('2', '3', 'latest') |
225 | + snap_risks = ('stable', 'candidate', 'beta', 'edge') |
226 | + return track in snap_tracks and risk in snap_risks |
227 | + |
228 | + |
229 | +def perform_snap_channel_change(new_channel): |
230 | + host.service_stop(SERVICE_NAME) |
231 | + # backup config file using the current version as a suffix |
232 | + cur_snap = snap.get_installed_version(SNAP_NAME) |
233 | + if os.path.exists(CONF_FILE): |
234 | + shutil.copy2(CONF_FILE, "{path}.{ext}".format(path=CONF_FILE, ext=cur_snap)) |
235 | + hookenv.status_set("maintenance", "Refreshing graylog snap {}".format(new_channel)) |
236 | + snap.refresh(SNAP_NAME, channel=new_channel) |
237 | + new_snap = snap.get_installed_version(SNAP_NAME) |
238 | + |
239 | + # Clear data related to the upgrade block |
240 | + unitdata.kv().unset("channel_block_type") |
241 | + |
242 | + # When the snap version changes, use the config file from the snap and |
243 | + # adjust states to ensure appropriate re-config happens. |
244 | + if cur_snap != new_snap: |
245 | + shutil.copy2(SNAP_CONF_FILE, CONF_FILE) |
246 | + data_changed("elasticsearch.relation", None) |
247 | + data_changed("mongodb.uri", None) |
248 | + remove_state("graylog.configured") |
249 | diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py |
250 | index 695b696..848e07d 100644 |
251 | --- a/lib/charms/layer/graylog/utils.py |
252 | +++ b/lib/charms/layer/graylog/utils.py |
253 | @@ -3,9 +3,13 @@ from urllib.parse import urlparse |
254 | import netifaces |
255 | |
256 | from charmhelpers.core import hookenv |
257 | -from charms.layer import snap |
258 | +from charms.layer.graylog.constants import SNAP_NAME |
259 | |
260 | -SNAP_NAME = "graylog" |
261 | +# Allow mocking of other layers during unit tests |
262 | +try: |
263 | + from charms.layer import snap |
264 | +except ImportError: |
265 | + snap = None |
266 | |
267 | |
268 | def get_api_port(): |
269 | diff --git a/reactive/graylog.py b/reactive/graylog.py |
270 | index 3db920c..716ebd9 100644 |
271 | --- a/reactive/graylog.py |
272 | +++ b/reactive/graylog.py |
273 | @@ -1,7 +1,6 @@ |
274 | import hashlib |
275 | import os |
276 | import re |
277 | -import shutil |
278 | import time |
279 | import yaml |
280 | from urllib.parse import urlparse |
281 | @@ -16,24 +15,26 @@ from charms.layer import snap |
282 | from charms.layer.graylog import ( |
283 | GraylogApi, |
284 | LogExtractPipeline, |
285 | - SNAP_NAME, |
286 | get_api_port, |
287 | get_api_url, |
288 | is_v2, |
289 | validate_api_uri |
290 | ) |
291 | - |
292 | - |
293 | -CONF_FILE = '/var/snap/graylog/common/server.conf' |
294 | -SNAP_CONF_FILE = '/snap/graylog/current/etc/graylog/server/server.conf' |
295 | -# /snap/graylog is a read-only squashfs so we use the initial settings |
296 | -# and write out an override to SERVER_DEFAULT_CONF_FILE |
297 | -SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server' |
298 | -SERVER_DEFAULT_CONF_FILE = '/var/snap/graylog/current/default-graylog-server' |
299 | -ELASTICSEARCH_DISCOVERY_PORT = '9300' |
300 | -SERVICE_NAME = 'snap.graylog.graylog' |
301 | -DEFAULT_REST_API_TIMEOUT = 120 |
302 | -NAGIOS_USERNAME = 'nagios' |
303 | +from charms.layer.graylog.snap_change import ( |
304 | + ChannelChangeStatus, |
305 | + get_channel_change_status, |
306 | + perform_snap_channel_change, |
307 | +) |
308 | +from charms.layer.graylog.constants import ( |
309 | + SNAP_NAME, |
310 | + CONF_FILE, |
311 | + SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE, |
312 | + SERVER_DEFAULT_CONF_FILE, |
313 | + ELASTICSEARCH_DISCOVERY_PORT, |
314 | + SERVICE_NAME, |
315 | + DEFAULT_REST_API_TIMEOUT, |
316 | + NAGIOS_USERNAME, |
317 | +) |
318 | |
319 | |
320 | class ApiTimeout(Exception): |
321 | @@ -89,37 +90,20 @@ def install_graylog(): |
322 | snap.install(SNAP_NAME, channel=channel) |
323 | |
324 | |
325 | -@when('snap.installed.graylog') |
326 | -@when('config.changed.channel') |
327 | +@when("snap.installed.graylog") |
328 | +@when("config.changed.channel") |
329 | def refresh_graylog(): |
330 | """Refresh the graylog snap. |
331 | |
332 | A change to the 'channel' config option may warrant a snap refresh. If so, |
333 | stop the service, backup the current config, and refresh. |
334 | """ |
335 | - channel = hookenv.config('channel') |
336 | - cur_snap = new_snap = snap.get_installed_version(SNAP_NAME) |
337 | - |
338 | - if channel: |
339 | - host.service_stop(SERVICE_NAME) |
340 | - # backup config file using the current version as a suffix |
341 | - if os.path.exists(CONF_FILE): |
342 | - shutil.copy2(CONF_FILE, '{path}.{ext}'.format(path=CONF_FILE, ext=cur_snap)) |
343 | - |
344 | - hookenv.status_set('maintenance', 'Refreshing graylog snap {}'.format(channel)) |
345 | - snap.refresh(SNAP_NAME, channel=channel) |
346 | - new_snap = snap.get_installed_version(SNAP_NAME) |
347 | + new_channel = hookenv.config("channel") |
348 | + if new_channel: |
349 | + if get_channel_change_status() == ChannelChangeStatus.MINOR_CHANGE: |
350 | + perform_snap_channel_change(new_channel) |
351 | else: |
352 | hookenv.log("Cannot refresh snap when the 'channel' config option is missing") |
353 | - |
354 | - # When the snap version changes, use the config file from the snap and |
355 | - # adjust states to ensure appropriate re-config happens. |
356 | - if cur_snap != new_snap: |
357 | - shutil.copy2(SNAP_CONF_FILE, CONF_FILE) |
358 | - data_changed('elasticsearch.relation', None) |
359 | - data_changed('mongodb.uri', None) |
360 | - remove_state('graylog.configured') |
361 | - |
362 | report_status() |
363 | |
364 | |
365 | @@ -272,11 +256,30 @@ def report_status(): |
366 | # Check for missing required applications |
367 | missing_apps = [] |
368 | if not es_connected: |
369 | - missing_apps.append('elasticsearch') |
370 | + missing_apps.append("elasticsearch") |
371 | if not mongodb_connected: |
372 | - missing_apps.append('mongodb') |
373 | - |
374 | - if missing_apps: |
375 | + missing_apps.append("mongodb") |
376 | + channel_change_status = get_channel_change_status() |
377 | + if channel_change_status not in (ChannelChangeStatus.NO_CHANGE, |
378 | + ChannelChangeStatus.MINOR_CHANGE): |
379 | + if channel_change_status == ChannelChangeStatus.LATEST_REQUESTED: |
380 | + msg = ('The "latest" track is not supported; revert the channel config ' |
381 | + 'change to unblock') |
382 | + elif channel_change_status == ChannelChangeStatus.INVALID: |
383 | + msg = 'Invalid snap channel: {}'.format( |
384 | + hookenv.config("channel")) |
385 | + elif channel_change_status == ChannelChangeStatus.UPGRADE_REQUESTED: |
386 | + msg = ("PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION: see the charm's " |
387 | + "README.md for more info") |
388 | + elif channel_change_status == ChannelChangeStatus.DOWNGRADE_REQUESTED: |
389 | + msg = ( |
390 | + "PACKAGE DOWNGRADES ARE NOT SUPPORTED BY THIS CHARM - to clear this " |
391 | + "message, set config 'snap_channel' to '{}'".format( |
392 | + snap.get_installed_channel(SNAP_NAME))) |
393 | + else: |
394 | + raise ValueError("Unexpected channel_block_type value: {!r}".format(channel_change_status)) |
395 | + hookenv.status_set("blocked", msg) |
396 | + elif missing_apps: |
397 | # Report blocked if we have missing apps |
398 | msg = "Missing relation to: {}".format(', '.join(missing_apps)) |
399 | hookenv.status_set('blocked', msg) |
400 | @@ -953,12 +956,15 @@ def get_default_graylog_client(): |
401 | |
402 | |
403 | def _verify_rest_api_is_alive(timeout=DEFAULT_REST_API_TIMEOUT): |
404 | + hookenv.log('Verifying REST API is alive...') |
405 | g = get_default_graylog_client() |
406 | url = '' # Will query using the base URL of the client, i.e. /api/ |
407 | resp = g.request(url) |
408 | start_ts = time.time() |
409 | while resp is None: |
410 | time.sleep(5) |
411 | + hookenv.log('Retrying REST API check...') |
412 | resp = g.request(url) |
413 | if time.time() - start_ts > timeout: |
414 | raise ApiTimeout() |
415 | + hookenv.log('REST API is up') |
416 | diff --git a/tests/requirements.txt b/tests/requirements.txt |
417 | index b7c9112..7345526 100644 |
418 | --- a/tests/requirements.txt |
419 | +++ b/tests/requirements.txt |
420 | @@ -1 +1,2 @@ |
421 | +tenacity |
422 | git+https://github.com/openstack-charmers/zaza.git#egg=zaza |
423 | diff --git a/tests/test_graylog_charm.py b/tests/test_graylog_charm.py |
424 | index 2f61b13..3c524a2 100644 |
425 | --- a/tests/test_graylog_charm.py |
426 | +++ b/tests/test_graylog_charm.py |
427 | @@ -4,6 +4,8 @@ import logging |
428 | import time |
429 | import unittest |
430 | |
431 | +import tenacity |
432 | + |
433 | import zaza.model as model |
434 | |
435 | from api import GraylogApi |
436 | @@ -126,6 +128,10 @@ class CharmOperationTest(BaseGraylogTest): |
437 | "query": '{}: "/var/log/cloud-init-output.log" AND ci-info'.format(self.file_field), |
438 | "keyword": "4 hours ago", |
439 | } |
440 | - |
441 | - resp = self.api.request("/search/universal/keyword", params=params) |
442 | - self.assertTrue(int(resp["total_results"]) > 0) |
443 | + for attempt in tenacity.Retrying( |
444 | + wait=tenacity.wait_fixed(10), # seconds |
445 | + stop=tenacity.stop_after_attempt(3), |
446 | + reraise=True): |
447 | + with attempt: |
448 | + resp = self.api.request("/search/universal/keyword", params=params) |
449 | + self.assertTrue(int(resp["total_results"]) > 0) |
450 | diff --git a/tests/test_graylog_upgrade.py b/tests/test_graylog_upgrade.py |
451 | new file mode 100644 |
452 | index 0000000..d2c09eb |
453 | --- /dev/null |
454 | +++ b/tests/test_graylog_upgrade.py |
455 | @@ -0,0 +1,159 @@ |
456 | +import time |
457 | +import unittest |
458 | + |
459 | +from zaza import model |
460 | + |
461 | + |
462 | +class UpgradeTest(unittest.TestCase): |
463 | + def setUp(self): |
464 | + self.unit_names = [unit.name for unit in model.get_units('graylog')] |
465 | + |
466 | + def test_01_channel_changed_to_newer_and_back(self): |
467 | + # Assumption: graylog channel is 2/stable before reaching this test |
468 | + self.assertEqual( |
469 | + model.get_application_config("graylog")["channel"].get("value"), "2/stable" |
470 | + ) |
471 | + |
472 | + # Upon attempting an upgrade, we should go to a blocked state, with a reference to |
473 | + # instructions on how to complete the upgrade. |
474 | + model.set_application_config("graylog", {"channel": "3/stable"}) |
475 | + for unit_name in self.unit_names: |
476 | + self._block_until( |
477 | + unit_name, |
478 | + "blocked", |
479 | + "PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION: see the charm's README.md " |
480 | + "for more info" |
481 | + ) |
482 | + |
483 | + # Let's roll back to 2/stable; at this point it's still supported |
484 | + model.set_application_config("graylog", {"channel": "2/stable"}) |
485 | + for unit_name in self.unit_names: |
486 | + self._block_until( |
487 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
488 | + ) |
489 | + |
490 | + def test_02_channel_changed_to_newer_and_finalized(self): |
491 | + # Assumption: graylog channel is 2/stable before reaching this test |
492 | + self.assertEqual( |
493 | + model.get_application_config("graylog")["channel"].get("value"), "2/stable" |
494 | + ) |
495 | + |
496 | + # Upon attempting an upgrade, we should go to a blocked state, with a reference to |
497 | + # instructions on how to complete the upgrade. |
498 | + model.set_application_config("graylog", {"channel": "3/stable"}) |
499 | + for unit_name in self.unit_names: |
500 | + self._block_until( |
501 | + unit_name, |
502 | + "blocked", |
503 | + "PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION: see the charm's README.md " |
504 | + "for more info" |
505 | + ) |
506 | + |
507 | + # Perform the "manual" upgrade |
508 | + for unit_name in self.unit_names: |
509 | + model.run_action(unit_name, "do-upgrade") |
510 | + |
511 | + # Verify that we've come back out the other side in one piece. |
512 | + for unit_name in self.unit_names: |
513 | + self._block_until( |
514 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
515 | + ) |
516 | + |
517 | + def test_03_channel_changed_to_older_and_back(self): |
518 | + # Assumption: graylog channel is 3/stable before reaching this test |
519 | + self.assertEqual( |
520 | + model.get_application_config("graylog")["channel"].get("value"), "3/stable" |
521 | + ) |
522 | + |
523 | + # Upon attempting a downgrade, we should go to a blocked state, explaining that downgrades are not supported. |
524 | + # (Reverting the config setting back will unblock the app.) |
525 | + model.set_application_config("graylog", {"channel": "2/stable"}) |
526 | + for unit_name in self.unit_names: |
527 | + self._block_until( |
528 | + unit_name, |
529 | + "blocked", |
530 | + "PACKAGE DOWNGRADES ARE NOT SUPPORTED BY THIS CHARM - to clear " |
531 | + "this message, set config 'snap_channel' to '3/stable'", |
532 | + ) |
533 | + |
534 | + # Let's roll back to 2/stable; at this point it's still supported |
535 | + model.set_application_config("graylog", {"channel": "3/stable"}) |
536 | + for unit_name in self.unit_names: |
537 | + self._block_until( |
538 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
539 | + ) |
540 | + |
541 | + def test_04_channel_changed_to_latest_and_back(self): |
542 | + # Assumption: graylog channel is 3/stable before reaching this test |
543 | + self.assertEqual( |
544 | + model.get_application_config("graylog")["channel"].get("value"), "3/stable" |
545 | + ) |
546 | + |
547 | + # We shouldn't allow upgrading to anything in the "latest" track; this may be newer *or* older than the current channel. |
548 | + # Require an explicit track, i.e. 2/* or 3/*.. |
549 | + model.set_application_config("graylog", {"channel": "latest/stable"}) |
550 | + for unit_name in self.unit_names: |
551 | + self._block_until( |
552 | + unit_name, |
553 | + "blocked", |
554 | + 'The "latest" track is not supported; revert the channel config change to ' |
555 | + "unblock", |
556 | + ) |
557 | + |
558 | + # Verify that we've come back out the other side in one piece. |
559 | + model.set_application_config("graylog", {"channel": "3/stable"}) |
560 | + for unit_name in self.unit_names: |
561 | + self._block_until( |
562 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
563 | + ) |
564 | + |
565 | + def test_05_channel_changed_to_invalid_value(self): |
566 | + # Assumption: graylog channel is 3/stable before reaching this test |
567 | + self.assertEqual( |
568 | + model.get_application_config("graylog")["channel"].get("value"), "3/stable" |
569 | + ) |
570 | + |
571 | + # The channel can be set to an arbitrary value; we should catch if the |
572 | + # channel is bogus. |
573 | + model.set_application_config("graylog", {"channel": "fake"}) |
574 | + for unit_name in self.unit_names: |
575 | + self._block_until( |
576 | + unit_name, |
577 | + "blocked", |
578 | + 'Invalid snap channel: fake', |
579 | + ) |
580 | + |
581 | + # Roll back |
582 | + model.set_application_config("graylog", {"channel": "3/stable"}) |
583 | + for unit_name in self.unit_names: |
584 | + self._block_until( |
585 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
586 | + ) |
587 | + |
588 | + def test_06_channel_changed_within_track(self): |
589 | + # Assumption: graylog channel is 3/stable before reaching this test |
590 | + self.assertEqual( |
591 | + model.get_application_config("graylog")["channel"].get("value"), "3/stable" |
592 | + ) |
593 | + |
594 | + # Snap refreshes within the same major series should be allowed. |
595 | + model.set_application_config("graylog", {"channel": "3/edge"}) |
596 | + # config changed may not run right away; wait a bit, else we might see the pre-update "active" state and think we're done. |
597 | + time.sleep(10) |
598 | + for unit_name in self.unit_names: |
599 | + self._block_until( |
600 | + unit_name, "active", "Ready with: filebeat, elasticsearch, mongodb" |
601 | + ) |
602 | + |
603 | + def _block_until( |
604 | + self, unit, expected_workload_status, expected_workload_status_message |
605 | + ): |
606 | + app = unit.split("/")[0] |
607 | + # Zaza doesn't presently seem to provide a way to check these 3 things |
608 | + # at the same time. I could write a helper to do this, but this may be "good |
609 | + # enough" for the time being. |
610 | + model.block_until_unit_wl_status(unit, expected_workload_status, timeout=300) |
611 | + model.block_until_wl_status_info_starts_with( |
612 | + app, expected_workload_status_message, timeout=300 |
613 | + ) |
614 | + model.wait_for_unit_idle(unit, timeout=300) |
615 | diff --git a/tests/tests.yaml b/tests/tests.yaml |
616 | index bd3f9ba..1b6afb6 100644 |
617 | --- a/tests/tests.yaml |
618 | +++ b/tests/tests.yaml |
619 | @@ -1,18 +1,23 @@ |
620 | charm_name: graylog |
621 | gate_bundles: |
622 | - - bionic-graylog2 |
623 | - - bionic-graylog3 |
624 | - - focal-graylog2 |
625 | - - focal-graylog3 |
626 | - - bionic-graylog2-ha |
627 | - - bionic-graylog3-ha |
628 | + - gl2: bionic-graylog2 |
629 | + - gl3: bionic-graylog3 |
630 | + - gl2: focal-graylog2 |
631 | + - gl3: focal-graylog3 |
632 | +# - gl2: bionic-graylog2-ha |
633 | +# - gl3: bionic-graylog3-ha |
634 | smoke_bundles: |
635 | - - bionic-graylog3 |
636 | + - gl3: bionic-graylog3 |
637 | dev_bundles: |
638 | - - bionic-graylog3 |
639 | + - gl3: bionic-graylog3 |
640 | tests: |
641 | - - tests.test_legacy.LegacyTests |
642 | - - tests.test_graylog_charm.CharmOperationTest |
643 | + - gl2: |
644 | + - tests.test_legacy.LegacyTests |
645 | + - tests.test_graylog_charm.CharmOperationTest |
646 | + - tests.test_graylog_upgrade.UpgradeTest |
647 | + - gl3: |
648 | + - tests.test_legacy.LegacyTests |
649 | + - tests.test_graylog_charm.CharmOperationTest |
650 | target_deploy_status: |
651 | filebeat: |
652 | workload-status: active |
653 | diff --git a/unit_tests/test_graylog.py b/unit_tests/test_graylog.py |
654 | index 4c1f8dd..38a8e35 100644 |
655 | --- a/unit_tests/test_graylog.py |
656 | +++ b/unit_tests/test_graylog.py |
657 | @@ -4,6 +4,10 @@ import tempfile |
658 | import unittest |
659 | from unittest import mock |
660 | from charms.reactive import set_state, is_state |
661 | +from charms.layer.graylog.snap_change import ( |
662 | + _is_channel_valid, |
663 | + ChannelChangeStatus, |
664 | +) |
665 | |
666 | # some dep layers only exists in the built charm; mock them out before |
667 | # the graylog imports since those depend on post-build charms.* layers |
668 | @@ -54,7 +58,7 @@ class TestInstallUpdateUpgrade(unittest.TestCase): |
669 | class TestRefresh(unittest.TestCase): |
670 | """Test the snap refresh handler.""" |
671 | |
672 | - @mock.patch('reactive.graylog.snap') |
673 | + @mock.patch('charms.layer.graylog.snap_change.snap') |
674 | @mock.patch('reactive.graylog.hookenv.config') |
675 | def test_no_channel(self, mock_config, mock_snap): |
676 | """Ensure a no-op if there is no 'channel' config.""" |
677 | @@ -65,38 +69,51 @@ class TestRefresh(unittest.TestCase): |
678 | self.assertFalse(mock_snap.refresh.called) |
679 | self.assertTrue(is_state('graylog.configured')) |
680 | |
681 | - @mock.patch('reactive.graylog.shutil') |
682 | - @mock.patch('reactive.graylog.os') |
683 | - @mock.patch('reactive.graylog.host') |
684 | + # test_change and test_no_change are getting a bit cumbersome due to |
685 | + # reworks/refactorings; we may want to consider dropping these 2 and instead |
686 | + # relying on functional coverage. |
687 | + |
688 | + @mock.patch('charms.layer.graylog.snap_change.unitdata') # Avoid unintended local state changes |
689 | + @mock.patch('reactive.graylog.unitdata') # Same as above |
690 | + @mock.patch('charms.layer.graylog.snap_change.os') |
691 | + @mock.patch('charms.layer.graylog.snap_change.snap') |
692 | @mock.patch('reactive.graylog.snap') |
693 | @mock.patch('reactive.graylog.hookenv.config') |
694 | - def test_no_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil): |
695 | + def test_no_change(self, mock_config, mock_snap, mock_snap2, mock_os, mock_ud, mock_ud2): |
696 | """Ensure no reconfiguration if the snap version does not change.""" |
697 | - mock_config.return_value = 'foo' |
698 | - mock_snap.get_installed_version.side_effect = ['2.foo', '2.foo'] |
699 | + mock_config.return_value = '2/stable' |
700 | + mock_snap.get_installed_version.side_effect = mock_snap2.get_installed_version.side_effect = ['2.1', '2.1'] |
701 | + mock_snap.get_installed_channel.return_value = mock_snap2.get_installed_channel.return_value = '2/stable' |
702 | mock_os.path.exists.return_value = True |
703 | - mock_shutil.copy2.return_value = 'foo' |
704 | + mock_ud.kv.return_value = mock_kv = mock.MagicMock() |
705 | + mock_kv.get.return_value = None # For update_status, mock that no channel_block_type has been set |
706 | set_state('graylog.configured') |
707 | refresh_graylog() |
708 | - self.assertTrue(mock_snap.refresh.called) |
709 | + self.assertFalse(mock_snap.refresh.called) |
710 | self.assertTrue(is_state('graylog.configured')) |
711 | |
712 | - @mock.patch('reactive.graylog.data_changed') |
713 | - @mock.patch('reactive.graylog.shutil.copy2') |
714 | - @mock.patch('reactive.graylog.os') |
715 | - @mock.patch('reactive.graylog.host') |
716 | + @mock.patch('charms.layer.graylog.snap_change.unitdata') # Avoid unintended local state changes |
717 | + @mock.patch('reactive.graylog.unitdata') # Same as above |
718 | + @mock.patch('charms.layer.graylog.snap_change.data_changed') |
719 | + @mock.patch('charms.layer.graylog.snap_change.shutil.copy2') |
720 | + @mock.patch('charms.layer.graylog.snap_change.os') |
721 | + @mock.patch('charms.layer.graylog.snap_change.host') |
722 | + @mock.patch('charms.layer.graylog.snap_change.snap') |
723 | @mock.patch('reactive.graylog.snap') |
724 | @mock.patch('reactive.graylog.hookenv.config') |
725 | - def test_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil, mock_data_changed): |
726 | + def test_change(self, mock_config, mock_snap, mock_snap2, mock_host, mock_os, mock_shutil, mock_data_changed, mock_ud, mock_ud2): |
727 | """Ensure calls and states when the snap has changed.""" |
728 | - mock_config.return_value = 'foo' |
729 | - mock_snap.get_installed_version.side_effect = ['2.foo', '3.foo'] |
730 | + mock_config.return_value = '2/stable' |
731 | + mock_snap.get_installed_version.side_effect = mock_snap2.get_installed_version.side_effect = ['2.1', '2.2'] |
732 | + mock_snap.get_installed_channel.return_value = mock_snap2.get_installed_channel.return_value = '2/candidate' |
733 | mock_os.path.exists.return_value = True |
734 | mock_shutil.return_value = 'foo' |
735 | + mock_ud.kv.return_value = mock_kv = mock.MagicMock() |
736 | + mock_kv.get.return_value = None # For update_status, mock that no channel_block_type has been set |
737 | set_state('graylog.configured') |
738 | refresh_graylog() |
739 | self.assertTrue(mock_host.service_stop.called) |
740 | - self.assertTrue(mock_snap.refresh.called) |
741 | + self.assertTrue(mock_snap2.refresh.called) |
742 | self.assertEqual(mock_shutil.call_count, 2) |
743 | self.assertEqual(mock_data_changed.call_count, 2) |
744 | self.assertFalse(is_state('graylog.configured')) |
745 | @@ -114,16 +131,19 @@ class TestConfig(unittest.TestCase): |
746 | configure_graylog() |
747 | self.assertTrue(mock_hookenv.status_set.called) |
748 | |
749 | + @mock.patch('reactive.graylog.get_channel_change_status') |
750 | @mock.patch('reactive.graylog.is_master') |
751 | @mock.patch('reactive.graylog.set_jvm_heap_size') |
752 | @mock.patch('reactive.graylog.validate_api_uri') |
753 | + @mock.patch('charms.layer.graylog.utils.is_v2') |
754 | @mock.patch('reactive.graylog.is_v2') |
755 | @mock.patch('reactive.graylog.set_conf') |
756 | @mock.patch('reactive.graylog.hookenv') |
757 | @mock.patch('reactive.graylog.os') |
758 | @mock.patch('reactive.graylog.unitdata') |
759 | def test_v2_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, |
760 | - mock_validate_uri, mock_jvm_heap, mock_is_master): |
761 | + mock_v2_2, mock_validate_uri, mock_jvm_heap, mock_is_master, |
762 | + mock_gccs): |
763 | """Ensure calls and states with various config values for v2.""" |
764 | class Config(dict): |
765 | def __init__(self, *args, **kw): |
766 | @@ -136,8 +156,9 @@ class TestConfig(unittest.TestCase): |
767 | 'web_listen_uri': None, |
768 | 'rest_transport_uri': 'foo/api', |
769 | 'web_endpoint_uri': 'bar/api'}) |
770 | - mock_v2.return_value = True |
771 | + mock_v2.return_value = mock_v2_2.return_value = True |
772 | mock_is_master.return_value = True |
773 | + mock_gccs.return_value = ChannelChangeStatus.NO_CHANGE |
774 | configure_graylog() |
775 | self.assertEqual(mock_validate_uri.call_count, 2) |
776 | self.assertTrue(mock_jvm_heap.called) |
777 | @@ -145,16 +166,19 @@ class TestConfig(unittest.TestCase): |
778 | self.assertTrue(mock_hookenv.open_port.called) |
779 | self.assertTrue(mock_is_master.called) |
780 | |
781 | + @mock.patch('reactive.graylog.get_channel_change_status') |
782 | @mock.patch('reactive.graylog.is_master') |
783 | @mock.patch('reactive.graylog.set_jvm_heap_size') |
784 | @mock.patch('reactive.graylog.validate_api_uri') |
785 | + @mock.patch('charms.layer.graylog.utils.is_v2') |
786 | @mock.patch('reactive.graylog.is_v2') |
787 | @mock.patch('reactive.graylog.set_conf') |
788 | @mock.patch('reactive.graylog.hookenv') |
789 | @mock.patch('reactive.graylog.os') |
790 | @mock.patch('reactive.graylog.unitdata') |
791 | def test_v3_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, |
792 | - mock_validate_uri, mock_jvm_heap, mock_is_master): |
793 | + mock_v2_2, mock_validate_uri, mock_jvm_heap, mock_is_master, |
794 | + mock_gccs): |
795 | """Ensure calls and states with various config values for v3.""" |
796 | class Config(dict): |
797 | def __init__(self, *args, **kw): |
798 | @@ -167,8 +191,9 @@ class TestConfig(unittest.TestCase): |
799 | 'web_listen_uri': None, |
800 | 'rest_transport_uri': 'foo/api', |
801 | 'web_endpoint_uri': 'bar/api'}) |
802 | - mock_v2.return_value = False |
803 | + mock_v2.return_value = mock_v2_2.return_value = False |
804 | mock_is_master.return_value = True |
805 | + mock_gccs.return_value = ChannelChangeStatus.NO_CHANGE |
806 | configure_graylog() |
807 | self.assertEqual(mock_validate_uri.call_count, 2) |
808 | self.assertTrue(mock_jvm_heap.called) |
809 | @@ -450,3 +475,19 @@ class TestSetJVMHeapSize(unittest.TestCase): |
810 | set_jvm_heap_size('6g', self.conf_file) |
811 | set_jvm_heap_size('8g', self.conf_file) |
812 | self.assertTrue(self.conf_equals(etc_default_conf.format(heap_twice))) |
813 | + |
814 | + |
815 | +class TestIsChannelValid(unittest.TestCase): |
816 | + |
817 | + def test_happy_path(self): |
818 | + for track in ('2', '3'): |
819 | + for risk in ('stable', 'candidate', 'beta', 'edge'): |
820 | + channel = '{}/{}'.format(track, risk) |
821 | + self.assertTrue(_is_channel_valid(channel)) |
822 | + |
823 | + def test_sad_path(self): |
824 | + for channel in ( |
825 | + 'fake/stable', # invalid track |
826 | + '2', # track w/o risk |
827 | + ): |
828 | + self.assertFalse(_is_channel_valid(channel)) |
829 | diff --git a/unit_tests/test_lib.py b/unit_tests/test_lib.py |
830 | index 565ffc1..b644280 100644 |
831 | --- a/unit_tests/test_lib.py |
832 | +++ b/unit_tests/test_lib.py |
833 | @@ -11,26 +11,26 @@ from charms.layer.graylog import ( |
834 | class TestLibraryUtils(unittest.TestCase): |
835 | """Test lib.charms.layer.graylog utils.""" |
836 | |
837 | - @mock.patch('charms.layer.graylog.snap.get_installed_version') |
838 | - def test_api(self, mock_snap): |
839 | + @mock.patch('charms.layer.graylog.utils.is_v2') |
840 | + def test_api(self, mock_v2): |
841 | """Validate the api port/url match what we expect for v2 and v3.""" |
842 | - mock_snap.return_value = '2.foo' |
843 | + mock_v2.return_value = True |
844 | self.assertEqual(get_api_port(), '9001') |
845 | self.assertEqual(get_api_url(), 'http://127.0.0.1:9001/api/') |
846 | |
847 | - mock_snap.return_value = '3.bar' |
848 | + mock_v2.return_value = False |
849 | self.assertEqual(get_api_port(), '9000') |
850 | self.assertEqual(get_api_url(), 'http://127.0.0.1:9000/api/') |
851 | |
852 | - @mock.patch('charms.layer.graylog.snap.get_installed_version') |
853 | - def test_validate_api_url(self, mock_snap): |
854 | + @mock.patch('charms.layer.graylog.utils.is_v2') |
855 | + def test_validate_api_url(self, mock_v2): |
856 | """Validate the URI suffix is valid for v2 and v3.""" |
857 | - mock_snap.return_value = '2.foo' |
858 | + mock_v2.return_value = True |
859 | self.assertEqual(validate_api_uri('foo'), 'foo/api/') |
860 | self.assertEqual(validate_api_uri('foo/'), 'foo/api/') |
861 | self.assertEqual(validate_api_uri('foo/api'), 'foo/api/') |
862 | |
863 | - mock_snap.return_value = '3.bar' |
864 | + mock_v2.return_value = False |
865 | self.assertEqual(validate_api_uri('foo'), 'foo/') |
866 | self.assertEqual(validate_api_uri('foo/'), 'foo/') |
867 | self.assertEqual(validate_api_uri('foo/api/'), 'foo/') |
868 | diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py |
869 | index 5bae816..8bfd61b 100644 |
870 | --- a/unit_tests/test_logextract.py |
871 | +++ b/unit_tests/test_logextract.py |
872 | @@ -44,16 +44,20 @@ testdata_stream_connect = { |
873 | |
874 | class TestGraylogPipelines(unittest.TestCase): |
875 | |
876 | + @mock.patch('charms.layer.graylog.logextract.is_v2') |
877 | @mock.patch('charms.layer.graylog.api.GraylogApi') |
878 | - def test_get_pipeline_for(self, mock_api): |
879 | + def test_get_pipeline_for(self, mock_api, mock_v2): |
880 | mock_api.request.return_value = testdata_simple_pipeline |
881 | + mock_v2.return_value = False |
882 | gpipes = GraylogPipelines(mock_api) |
883 | test = gpipes.get_for_title('juju-logextract-pipeline') |
884 | self.assertEqual(test['title'], 'juju-logextract-pipeline') |
885 | |
886 | + @mock.patch('charms.layer.graylog.logextract.is_v2') |
887 | @mock.patch('charms.layer.graylog.api.GraylogApi') |
888 | - def test_set_pipeline(self, mock_api): |
889 | + def test_set_pipeline(self, mock_api, mock_v2): |
890 | mock_api.request.return_value = testdata_simple_pipeline |
891 | + mock_v2.return_value = False |
892 | gpipes = GraylogPipelines(mock_api) |
893 | gpipes.set('juju-logextract-pipeline', testdata_simple_pipeline_source) |
894 | self.assertEqual(mock_api.request.call_args_list[1][1]['data']['title'], 'juju-logextract-pipeline') |
895 | @@ -61,9 +65,11 @@ class TestGraylogPipelines(unittest.TestCase): |
896 | |
897 | class TestGraylogRules(unittest.TestCase): |
898 | |
899 | + @mock.patch('charms.layer.graylog.logextract.is_v2') |
900 | @mock.patch('charms.layer.graylog.api.GraylogApi') |
901 | - def test_get_rules(self, mock_api): |
902 | + def test_get_rules(self, mock_api, mock_v2): |
903 | mock_api.request.return_value = testdata_rules |
904 | + mock_v2.return_value = False |
905 | grules = GraylogRules(mock_api) |
906 | rules = grules.get() |
907 | self.assertEqual(rules[0]['title'], "from ceph mon") |
908 | @@ -92,8 +98,10 @@ class TestLogExtractPipeline(unittest.TestCase): |
909 | t = logext.parse_rule_title(testdata_rules[0]['source']) |
910 | self.assertEqual(t, 'from ceph mon') |
911 | |
912 | + @mock.patch('charms.layer.graylog.logextract.is_v2') |
913 | @mock.patch('charms.layer.graylog.api.GraylogApi') |
914 | - def test_set_rules(self, mock_api): |
915 | + def test_set_rules(self, mock_api, mock_v2): |
916 | + mock_v2.return_value = True |
917 | logext = LogExtractPipeline(mock_api) |
918 | logext.set_rules([testdata_rules[0]['source']]) |
919 | exp = [mock.call('/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline')] |
Hey, some comments inline. Think we should also discuss some of the behaviour details to get to a common view where want to go
cheers,
peter.