Merge ~vultaire/charm-graylog:protected-upgrades into charm-graylog:master

Proposed by Paul Goins
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)
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.

To post a comment you must log in.
Revision history for this message
Peter Sabaini (peter-sabaini) wrote :

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.

review: Needs Information
Revision history for this message
Joe Guo (guoqiao) wrote :

A few inline comments, otherwise looks good to me.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
Paul Goins (vultaire) wrote :

Replied to comments.

Revision history for this message
Paul Goins (vultaire) :
Revision history for this message
Paul Goins (vultaire) :
Revision history for this message
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.

Revision history for this message
Drew Freiberger (afreiberger) wrote :

Thanks for the cleanup, looks good to me.

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

I've found a few issues that could be fixed as follows:
https://pastebin.ubuntu.com/p/pzhghDxGJ7/

In the Makefile, /tmp/graylog-built should be used or this error will happen:
"""
2020-08-03 12:19:46 [INFO] Deploying bundle './tests/bundles/bionic-graylog2.yaml' on to 'zaza-d99cf0eb30e2' model
2020-08-03 12:19:46 [INFO] Rendered template '<Template 'local-charm-overlay.yaml.j2'>' to file '/tmp/tmp77_zr8xr/local-charm-overlay.yaml'
2020-08-03 12:19:46 [INFO] Deploying overlay '/tmp/tmp77_zr8xr/local-charm-overlay.yaml' on to 'zaza-d99cf0eb30e2' model
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-built/builds/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://github.com/openstack-charmers/zaza-openstack-tests/blob/3f7d82c8509749e72ae3818079e942f88688e24f/zaza/openstack/charm_tests/neutron_arista/tests.py#L41

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).

review: Needs Fixing
Revision history for this message
Paul Goins (vultaire) wrote :

Replied to some of the comments. Will think this over.

Revision history for this message
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).

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
Xav Paice (xavpaice) wrote :
Download full text (6.9 KiB)

`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_channel_changed_to_newer_and_finalized (tests.test_graylog_upgrade.UpgradeTest)
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/test_graylog_upgrade.py", line 58, in test_02_channel_changed_to_newer_and_finalized
2020-08-10 11:17:40 [INFO] self._block_until(
2020-08-10 11:17:40 [INFO] File "./tests/test_graylog_upgrade.py", line 155, in _block_until
2020-08-10 11:17:40 [INFO] model.block_until_unit_wl_status(unit, expected_workload_status, timeout=300)
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 48, in _wrapper
2020-08-10 11:17:40 [INFO] return run(_run_it())
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 36, in run
2020-08-10 11:17:40 [INFO] return task.result()
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 47, in _run_it
2020-08-10 11:17:40 [INFO] return await f(*args, **kwargs)
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-packages/zaza/model.py", line 1558, in async_block_until_unit_wl_status
2020-08-10 11:17:40 [INFO] await async_block_until(_unit_status, timeout=timeout)
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-packages/zaza/model.py", line 1240, in async_block_until
2020-08-10 11:17:40 [INFO] await asyncio.wait_for(_block(), timeout, loop=loop)
2020-08-10 11:17:40 [INFO] File "/usr/lib/python3.8/asyncio/tasks.py", line 490, in wait_for
2020-08-10 11:17:40 [INFO] raise exceptions.TimeoutError()
2020-08-10 11:17:40 [INFO] asyncio.exceptions.TimeoutError
2020-08-10 11:17:40 [INFO] ======================================================================
2020-08-10 11:17:40 [INFO] ERROR: test_03_channel_changed_to_older_and_back (tests.test_graylog_upgrade.UpgradeTest)
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/test_graylog_upgrade.py", line 72, in test_03_channel_changed_to_older_and_back
2020-08-10 11:17:40 [INFO] self._block_until(
2020-08-10 11:17:40 [INFO] File "./tests/test_graylog_upgrade.py", line 155, in _block_until
2020-08-10 11:17:40 [INFO] model.block_until_unit_wl_status(unit, expected_workload_status, timeout=300)
2020-08-10 11:17:40 [INFO] File "/home/xav/charms/charm-graylog/.tox/func/lib/python3.8/site-pac...

Read more...

review: Needs Fixing
Revision history for this message
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.

review: Approve
Revision history for this message
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).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 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
16diff --git a/actions.yaml b/actions.yaml
17index 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.
37diff --git a/actions/actions.py b/actions/actions.py
38index 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
123diff --git a/actions/do-upgrade b/actions/do-upgrade
124new file mode 120000
125index 0000000..405a394
126--- /dev/null
127+++ b/actions/do-upgrade
128@@ -0,0 +1 @@
129+actions.py
130\ No newline at end of file
131diff --git a/lib/charms/layer/graylog/constants.py b/lib/charms/layer/graylog/constants.py
132new file mode 100644
133index 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'
148diff --git a/lib/charms/layer/graylog/snap_change.py b/lib/charms/layer/graylog/snap_change.py
149new file mode 100644
150index 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")
249diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py
250index 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():
269diff --git a/reactive/graylog.py b/reactive/graylog.py
270index 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')
416diff --git a/tests/requirements.txt b/tests/requirements.txt
417index 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
423diff --git a/tests/test_graylog_charm.py b/tests/test_graylog_charm.py
424index 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)
450diff --git a/tests/test_graylog_upgrade.py b/tests/test_graylog_upgrade.py
451new file mode 100644
452index 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)
615diff --git a/tests/tests.yaml b/tests/tests.yaml
616index 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
653diff --git a/unit_tests/test_graylog.py b/unit_tests/test_graylog.py
654index 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))
829diff --git a/unit_tests/test_lib.py b/unit_tests/test_lib.py
830index 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/')
868diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py
869index 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')]

Subscribers

People subscribed via source and target branches

to all changes: