Merge ~vultaire/charm-graylog:upgrade-tests-and-reworks-20200724 into charm-graylog:master
- Git
- lp:~vultaire/charm-graylog
- upgrade-tests-and-reworks-20200724
- Merge into master
Status: | Rejected |
---|---|
Rejected by: | Paul Goins |
Proposed branch: | ~vultaire/charm-graylog:upgrade-tests-and-reworks-20200724 |
Merge into: | charm-graylog:master |
Prerequisite: | ~vultaire/charm-graylog:es-and-mongo-version-checks-disabled-v2 |
Diff against target: |
726 lines (+607/-10) (has conflicts) 13 files modified
lib/charms/layer/graylog/api.py (+231/-0) lib/charms/layer/graylog/utils.py (+0/-1) lib/charms/layer/graylog/version_check.py (+2/-8) src/tests/functional/tests/__init__.py (+1/-1) src/tests/functional/tests/bundles/bionic-graylog2.yaml (+17/-0) src/tests/functional/tests/bundles/bionic-graylog3.yaml (+17/-0) src/tests/unit/requirements.txt (+1/-0) tests/requirements.txt (+7/-0) tests/shared.py (+40/-0) tests/test_graylog_charm.py (+117/-0) tests/test_legacy.py (+83/-0) tests/test_upgrade.py (+65/-0) tests/tests.yaml (+26/-0) Conflict in lib/charms/layer/graylog/api.py Conflict in lib/charms/layer/graylog/logextract.py Conflict in lib/charms/layer/graylog/utils.py Conflict in reactive/graylog.py Conflict in src/lib/charms/layer/graylog/api.py Conflict in src/lib/charms/layer/graylog/logextract.py Conflict in src/lib/charms/layer/graylog/utils.py Conflict in src/tests/functional/tests/test_graylog_charm.py Conflict in src/tests/functional/tests/test_legacy.py Conflict in src/tests/functional/tests/tests.yaml Conflict in tests/requirements.txt Conflict in tests/test_graylog_charm.py Conflict in tests/test_legacy.py Conflict in tests/tests.yaml Conflict in unit_tests/test_lib.py |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Goins | Disapprove | ||
Drew Freiberger (community) | Needs Fixing | ||
Review via email: mp+388097@code.launchpad.net |
Commit message
Description of the change
Paul Goins (vultaire) wrote : | # |
Paul Goins (vultaire) wrote : | # |
Unit and functional tests should all now be passing. I'm doing one more clean run of functional tests, after which I'll mark this as ready for review.
Paul Goins (vultaire) wrote : | # |
Tests are confirmed passing. Ready for review.
Drew Freiberger (afreiberger) wrote : | # |
Much of this code appears to have landed since this MR. can you please review for bits added in this that may be missing from master? pathing has changed during 20.08 release cycle and will require major rebase and rework.
Drew Freiberger (afreiberger) : | # |
Paul Goins (vultaire) wrote : | # |
Abandoning in favor of pipelines-2021.
Unmerged commits
- 5714aec... by Paul Goins
-
Oops; rename problem. Fixed.
- 9663997... by Paul Goins
-
Fixed unit test breakage
Yes, I could have got fancy and tried to avoid the netifaces dependency,
but there seemed little point; the library is small and I'd rather keep
the code looking clean without doing tricks such as performing imports
in the functions which use the libraries. - 3fb2191... by Paul Goins
-
Minor changes to test_upgrade.py
- 79f3a4a... by Paul Goins
-
Added another prereq
- 2b1aebd... by Paul Goins
-
Removed some test helper bits in favor of installing libs for testing
- fc1d113... by Paul Goins
-
Adding testing prereqs
- 2795245... by Paul Goins
-
Fixed mistake in reworked is_v2() helper
- ea64132... by Paul Goins
-
Updates to shared test code:
* is_v2() should be based upon the config of the graylog unit. This
is just in case a bug installs the wrong version of Graylog - if we
test based upon the installed version, we'll miss bugs, whereas if
we test based upon the config, that is less likely.* I also tried to clean things up to remove the hardcoded 'graylog'
and 'graylog/0' dependencies. - 504b1f4... by Paul Goins
-
Fixed test_graylog_
charm.py, updated shared code * Set @staticmethod/
@classmethod decorators on base class methods
to enable use during setUpClass().* Updated test_graylog_
charm.py to leverage the shared helpers to
simplify setup and replace API constant use with get_api_port(). - 336e271... by Paul Goins
-
Extracted new shared test logic module
* Extracted BaseTest superclass for sharing several helper functions.
* DEFAULT_API_PORT and DEFAULT_WEB_PORT also moved here.
Preview Diff
1 | diff --git a/lib/charms/layer/graylog/api.py b/lib/charms/layer/graylog/api.py |
2 | new file mode 100644 |
3 | index 0000000..825a70e |
4 | --- /dev/null |
5 | +++ b/lib/charms/layer/graylog/api.py |
6 | @@ -0,0 +1,231 @@ |
7 | +<<<<<<< lib/charms/layer/graylog/api.py |
8 | +======= |
9 | +# |
10 | +# " " |
11 | +# mmm m m mmm m m |
12 | +# # # # # # # |
13 | +# # # # # # # |
14 | +# # "mm"# # "mm"# |
15 | +# # # |
16 | +# "" "" |
17 | +# This file is managed by Juju. Local changes will be lost. |
18 | +import json |
19 | +import os |
20 | +import requests |
21 | + |
22 | +from charmhelpers.core import hookenv |
23 | +from charmhelpers.core.hookenv import log |
24 | + |
25 | + |
26 | +def get_ignore_indexer_failures_file(): |
27 | + return "/usr/local/lib/nagios/plugins/ignore_indexer_failures.timestamp" |
28 | + |
29 | + |
30 | +class GraylogApi: |
31 | + |
32 | + def __init__(self, base_url, username, password, token_name='graylog-api'): |
33 | + self.base_url = base_url |
34 | + if not base_url.endswith('/'): |
35 | + self.base_url += '/' |
36 | + self.username = username |
37 | + self.password = password |
38 | + self.auth = (self.username, self.password) |
39 | + self.token_name = token_name |
40 | + self.token = None |
41 | + self.input_types = None |
42 | + self.req_timeout = 3 |
43 | + self.req_retries = 4 |
44 | + |
45 | + def request(self, path, method='GET', data={}, params=None): |
46 | + if path.startswith('/'): |
47 | + path = path[1:] |
48 | + url = '{}{}?pretty=true'.format(self.base_url, path) |
49 | + headers = { |
50 | + 'Accept': 'application/json', |
51 | + 'Content-Type': 'application/json', |
52 | + 'Accept-Encoding': '', |
53 | + 'X-Requested-By': 'Graylog Charm' |
54 | + } |
55 | + if data: |
56 | + data = json.dumps(data, indent=True) |
57 | + tries = 0 |
58 | + while tries < self.req_retries: |
59 | + tries += 1 |
60 | + try: |
61 | + resp = requests.api.request(method, url, auth=self.auth, |
62 | + data=data, params=params, |
63 | + headers=headers, timeout=self.req_timeout) |
64 | + if resp.ok: |
65 | + if method == 'DELETE': |
66 | + return True |
67 | + return json.loads(resp.content.decode('utf-8')) |
68 | + else: |
69 | + msg = 'API error code: {}'.format(resp.status_code) |
70 | + log(msg) |
71 | + hookenv.status_set('blocked', msg) |
72 | + except Exception as ex: |
73 | + msg = 'Error calling graylog api: {}'.format(ex) |
74 | + log(msg) |
75 | + |
76 | + def token_get(self, token_name=None, halt=False): |
77 | + if self.token: |
78 | + return self.token |
79 | + if not token_name: |
80 | + token_name = self.token_name |
81 | + if self.password == 'token': |
82 | + self.token = self.username |
83 | + self.auth = (self.token, 'token') |
84 | + return self.token |
85 | + url = 'users/{}/tokens'.format(self.username) |
86 | + resp = self.request(url) |
87 | + if not resp: |
88 | + return |
89 | + for token in resp['tokens']: |
90 | + if token['name'] == token_name: |
91 | + self.token = token['token'] |
92 | + self.auth = (token['token'], 'token') |
93 | + return self.token |
94 | + |
95 | + # None found, let's create a new |
96 | + url = '{}/{}'.format(url, token_name) |
97 | + resp = self.request(url, method='POST') |
98 | + if not halt: |
99 | + return self.token_get(token_name=token_name, halt=True) |
100 | + else: |
101 | + hookenv.status_set('blocked', 'Cannot get API token.') |
102 | + return None # Don't loop until we run out of memory. |
103 | + |
104 | + def index_set_get(self, index_id=None): |
105 | + if not self.token: |
106 | + self.token_get() |
107 | + url = 'system/indices/index_sets' |
108 | + if index_id: |
109 | + return self.request('{}/{}'.format(url, index_id)) |
110 | + resp = self.request(url) |
111 | + if resp: |
112 | + return resp.get('index_sets', None) |
113 | + |
114 | + def index_set_update(self, index_id, data): |
115 | + if not self.token: |
116 | + self.token_get() |
117 | + url = 'system/indices/index_sets/{}'.format(index_id) |
118 | + return self.request(url, method='PUT', data=data) |
119 | + |
120 | + def log_input_get(self, input_id=None): |
121 | + if not self.token: |
122 | + self.token_get() |
123 | + url = 'system/inputs' |
124 | + if input_id: |
125 | + return self.request('{}/{}'.format(url, input_id)) |
126 | + resp = self.request(url) |
127 | + if resp: |
128 | + return resp.get('inputs', None) |
129 | + |
130 | + def log_input_update(self, input_id=None, data=None): |
131 | + if not self.token: |
132 | + self.token_get() |
133 | + url = 'system/inputs/' |
134 | + if input_id: |
135 | + method = 'PUT' |
136 | + url += input_id |
137 | + else: |
138 | + method = 'POST' |
139 | + return self.request(url, method=method, data=data) |
140 | + |
141 | + def log_input_remove(self, input_id): |
142 | + if not self.token: |
143 | + self.token_get() |
144 | + url = 'system/inputs/{}'.format(input_id) |
145 | + return self.request(url, method='DELETE') |
146 | + |
147 | + def map_log_input_type(self, type): |
148 | + """ |
149 | + Map specified int types to Gray's input type objects. Examples: |
150 | + |
151 | + "Beats" -> org.graylog.plugins.beats.BeatsInput |
152 | + "Raw TCP" -> org.graylog2.inputs.raw.tcp.RawTCPInput |
153 | + "Syslog UDP" -> org.graylog2.inputs.syslog.udp.SyslogUDPInput |
154 | + """ |
155 | + if not self.input_types: |
156 | + url = 'system/inputs/types' |
157 | + result = self.request(url) |
158 | + if result is not None: |
159 | + self.input_types = result['types'] |
160 | + else: |
161 | + return None |
162 | + for k, v in self.input_types.items(): |
163 | + if v.lower().find(type.lower()) != -1: |
164 | + return k |
165 | + # For raw input types, map something like "Raw TCP" and "Raw UDP" |
166 | + # instead of requiring the exact name, "Raw/Plaintext TCP". |
167 | + if k.lower().find('.'.join(type.split()).lower()) != -1: |
168 | + return k |
169 | + return None |
170 | + |
171 | + def cluster_get(self): |
172 | + if not self.token: |
173 | + self.token_get() |
174 | + url = 'cluster' |
175 | + return self.request(url) |
176 | + |
177 | + def user_get(self, username): |
178 | + if not self.token: |
179 | + self.token_get() |
180 | + url = 'users/{}'.format(username) |
181 | + return self.request(url) |
182 | + |
183 | + def user_create(self, username, password, read_only=True): |
184 | + if not self.token: |
185 | + self.token_get() |
186 | + user = self.user_get(username) |
187 | + if user: |
188 | + return user |
189 | + url = 'users/' |
190 | + d = { |
191 | + 'username': username, |
192 | + 'password': password, |
193 | + 'full_name': username, |
194 | + 'email': '{}@local'.format(username), |
195 | + 'permissions': [], |
196 | + } |
197 | + self.request(url, method='POST', data=d) |
198 | + return self.user_get(username) |
199 | + |
200 | + def user_permissions_clear(self, username): |
201 | + if not self.token: |
202 | + self.token_get() |
203 | + user = self.user_get(username) |
204 | + if not user: |
205 | + return |
206 | + url = 'users/{}/permissions'.format(username) |
207 | + self.request(url, method='DELETE') |
208 | + |
209 | + def user_permissions_set(self, username, permission): |
210 | + if not self.token: |
211 | + self.token_get() |
212 | + user = self.user_get(username) |
213 | + if not user: |
214 | + return |
215 | + url = 'users/{}/permissions'.format(username) |
216 | + self.request(url, method='PUT', data={'permissions': permission}) |
217 | + |
218 | + def indexer_cluster_health(self): |
219 | + if not self.token: |
220 | + self.token_get() |
221 | + return self.request('system/indexer/cluster/health') |
222 | + |
223 | + def indexer_failures_count(self, since): |
224 | + if not self.token: |
225 | + self.token_get() |
226 | + return self.request('system/indexer/failures/count', params={'since': since}) |
227 | + |
228 | + def journal_state(self): |
229 | + if not self.token: |
230 | + self.token_get() |
231 | + return self.request('system/journal') |
232 | + |
233 | + def notifications_get(self): |
234 | + if not self.token: |
235 | + self.token_get() |
236 | + return self.request('system/notifications') |
237 | +>>>>>>> lib/charms/layer/graylog/api.py |
238 | diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py |
239 | index 7e9021e..29ec527 100644 |
240 | --- a/lib/charms/layer/graylog/utils.py |
241 | +++ b/lib/charms/layer/graylog/utils.py |
242 | @@ -5,7 +5,6 @@ from urllib.parse import urlparse |
243 | import netifaces |
244 | |
245 | from charmhelpers.core import hookenv |
246 | -from charms.layer import snap |
247 | from .version_check import is_v2 |
248 | |
249 | |
250 | diff --git a/lib/charms/layer/graylog/version_check.py b/lib/charms/layer/graylog/version_check.py |
251 | index 23aafd5..9ee5adc 100644 |
252 | --- a/lib/charms/layer/graylog/version_check.py |
253 | +++ b/lib/charms/layer/graylog/version_check.py |
254 | @@ -1,13 +1,7 @@ |
255 | import os |
256 | |
257 | -if os.environ.get('JUJU_UNIT_NAME'): |
258 | - charm = True |
259 | -else: |
260 | - charm = False |
261 | - |
262 | -if charm: |
263 | - from charmhelpers.core import hookenv |
264 | - from charms.layer import snap |
265 | +from charmhelpers.core import hookenv |
266 | +from charms.layer import snap |
267 | |
268 | |
269 | SNAP_NAME = 'graylog' |
270 | diff --git a/src/tests/functional/tests/__init__.py b/src/tests/functional/tests/__init__.py |
271 | index 351a8a2..32b2652 100644 |
272 | --- a/src/tests/functional/tests/__init__.py |
273 | +++ b/src/tests/functional/tests/__init__.py |
274 | @@ -11,4 +11,4 @@ def _add_path(path): |
275 | |
276 | built_charm = pathlib.Path(os.environ["CHARM_BUILD_DIR"]) / "graylog" |
277 | # Need to avoid importing c.l.graylog as it wants charmhelpers etc. |
278 | -_add_path(str(built_charm / "lib/charms/layer/graylog")) |
279 | +_add_path(str(built_charm / "lib")) |
280 | diff --git a/src/tests/functional/tests/bundles/bionic-graylog2.yaml b/src/tests/functional/tests/bundles/bionic-graylog2.yaml |
281 | index bd256db..b1318f0 100644 |
282 | --- a/src/tests/functional/tests/bundles/bionic-graylog2.yaml |
283 | +++ b/src/tests/functional/tests/bundles/bionic-graylog2.yaml |
284 | @@ -30,6 +30,19 @@ applications: |
285 | nrpe: |
286 | charm: cs:nrpe |
287 | |
288 | + old-graylog: |
289 | + charm: cs:graylog-32 |
290 | + num_units: 1 |
291 | + series: bionic |
292 | + |
293 | + elastic2: |
294 | + charm: cs:elasticsearch |
295 | + num_units: 1 |
296 | + |
297 | + mongo2: |
298 | + charm: cs:mongodb |
299 | + num_units: 1 |
300 | + |
301 | relations: |
302 | - - ubuntu |
303 | - filebeat |
304 | @@ -43,3 +56,7 @@ relations: |
305 | - haproxy |
306 | - - graylog |
307 | - nrpe |
308 | + - - old-graylog |
309 | + - mongo2 |
310 | + - - old-graylog |
311 | + - elastic2 |
312 | diff --git a/src/tests/functional/tests/bundles/bionic-graylog3.yaml b/src/tests/functional/tests/bundles/bionic-graylog3.yaml |
313 | index 596cf7d..1f6020a 100644 |
314 | --- a/src/tests/functional/tests/bundles/bionic-graylog3.yaml |
315 | +++ b/src/tests/functional/tests/bundles/bionic-graylog3.yaml |
316 | @@ -30,6 +30,19 @@ applications: |
317 | nrpe: |
318 | charm: cs:nrpe |
319 | |
320 | + old-graylog: |
321 | + charm: cs:graylog-32 |
322 | + num_units: 1 |
323 | + series: bionic |
324 | + |
325 | + elastic2: |
326 | + charm: cs:elasticsearch |
327 | + num_units: 1 |
328 | + |
329 | + mongo2: |
330 | + charm: cs:mongodb |
331 | + num_units: 1 |
332 | + |
333 | relations: |
334 | - - ubuntu |
335 | - filebeat |
336 | @@ -43,3 +56,7 @@ relations: |
337 | - haproxy |
338 | - - graylog |
339 | - nrpe |
340 | + - - old-graylog |
341 | + - mongo2 |
342 | + - - old-graylog |
343 | + - elastic2 |
344 | diff --git a/src/tests/unit/requirements.txt b/src/tests/unit/requirements.txt |
345 | index c465d62..d39b4a3 100644 |
346 | --- a/src/tests/unit/requirements.txt |
347 | +++ b/src/tests/unit/requirements.txt |
348 | @@ -5,3 +5,4 @@ netifaces |
349 | pytest |
350 | pytest-cov |
351 | requests |
352 | +netifaces |
353 | diff --git a/tests/requirements.txt b/tests/requirements.txt |
354 | new file mode 100644 |
355 | index 0000000..27331d7 |
356 | --- /dev/null |
357 | +++ b/tests/requirements.txt |
358 | @@ -0,0 +1,7 @@ |
359 | +<<<<<<< tests/requirements.txt |
360 | +======= |
361 | +git+https://github.com/openstack-charmers/zaza.git#egg=zaza |
362 | +charmhelpers |
363 | +charms.reactive |
364 | +netifaces |
365 | +>>>>>>> tests/requirements.txt |
366 | diff --git a/tests/shared.py b/tests/shared.py |
367 | new file mode 100644 |
368 | index 0000000..9ea5c5a |
369 | --- /dev/null |
370 | +++ b/tests/shared.py |
371 | @@ -0,0 +1,40 @@ |
372 | +from zaza import model |
373 | +from zaza.utilities import juju |
374 | + |
375 | +from charms.layer.graylog.api import GraylogApi |
376 | + |
377 | + |
378 | +DEFAULT_API_PORT = "9001" # Graylog 2 only |
379 | +DEFAULT_WEB_PORT = "9000" |
380 | + |
381 | + |
382 | +class BaseTest: |
383 | + |
384 | + app_name = 'graylog' |
385 | + |
386 | + @classmethod |
387 | + def is_v2(cls, app_name=app_name): |
388 | + config = model.get_application_config(app_name) |
389 | + return config['channel']['value'].startswith('2/') |
390 | + |
391 | + @classmethod |
392 | + def get_api_port(cls): |
393 | + return DEFAULT_API_PORT if cls.is_v2() else DEFAULT_WEB_PORT |
394 | + |
395 | + @classmethod |
396 | + def get_graylog_clients(cls, app_name=app_name): |
397 | + graylog_units = juju.get_full_juju_status().applications[app_name]["units"] |
398 | + graylog_addrs = [unit["public-address"] for unit in graylog_units.values()] |
399 | + port = cls.get_api_port() |
400 | + password = cls.get_graylog_admin_password() |
401 | + return [ |
402 | + GraylogApi("http://{}:{}/api".format(graylog_addr, port), "admin", password) |
403 | + for graylog_addr in graylog_addrs |
404 | + ] |
405 | + |
406 | + @classmethod |
407 | + def get_graylog_admin_password(cls, unit=None): |
408 | + if unit is None: |
409 | + unit = '{}/0'.format(cls.app_name) |
410 | + result = model.run_action(unit, "show-admin-password") |
411 | + return result.data["results"]["admin-password"] |
412 | diff --git a/tests/test_graylog_charm.py b/tests/test_graylog_charm.py |
413 | new file mode 100644 |
414 | index 0000000..ccd8fed |
415 | --- /dev/null |
416 | +++ b/tests/test_graylog_charm.py |
417 | @@ -0,0 +1,117 @@ |
418 | +<<<<<<< tests/test_graylog_charm.py |
419 | +======= |
420 | +"""Encapsulate graylog testing.""" |
421 | + |
422 | +import logging |
423 | +import time |
424 | +import unittest |
425 | + |
426 | +import zaza.model as model |
427 | + |
428 | +from tests.shared import BaseTest |
429 | + |
430 | + |
431 | +CURL_TIMEOUT = 180 |
432 | +REQ_TIMEOUT = 12 |
433 | + |
434 | + |
435 | +class BaseGraylogTest(unittest.TestCase, BaseTest): |
436 | + """Base for Graylog charm tests.""" |
437 | + |
438 | + @classmethod |
439 | + def setUpClass(cls): |
440 | + super(BaseGraylogTest, cls).setUpClass() |
441 | + cls.model_name = model.get_juju_model() |
442 | + cls.application_name = "graylog" |
443 | + cls.lead_unit_name = model.get_lead_unit_name( |
444 | + cls.application_name, model_name=cls.model_name |
445 | + ) |
446 | + cls.units = model.get_units( |
447 | + cls.application_name, model_name=cls.model_name |
448 | + ) |
449 | + logging.debug("Leader unit is {}".format(cls.lead_unit_name)) |
450 | + cls.api = cls.get_graylog_clients()[0] |
451 | + cls.api.req_timeout = ( |
452 | + REQ_TIMEOUT |
453 | + ) # try harder to get an api connection to cater for overloaded testsystems |
454 | + logging.debug("API at {}".format(cls.api.base_url)) |
455 | + |
456 | + # account for the differences between graylog2 and 3 |
457 | + if cls.is_v2(): |
458 | + cls.file_field = "file" |
459 | + cls.expected_plugins = { |
460 | + "org.graylog.plugins.pipelineprocessor.ProcessorPlugin", |
461 | + "org.graylog.plugins.beats.BeatsInputPlugin", |
462 | + "org.graylog.plugins.collector.CollectorPlugin", |
463 | + |
464 | + } |
465 | + else: # graylog 3+ |
466 | + cls.file_field = "filebeat_log_file_path" |
467 | + cls.expected_plugins = { |
468 | + "org.graylog.aws.AWSPlugin", |
469 | + "org.graylog.plugins.collector.CollectorPlugin", |
470 | + "org.graylog.plugins.threatintel.ThreatIntelPlugin" |
471 | + } |
472 | + |
473 | +class CharmOperationTest(BaseGraylogTest): |
474 | + """Verify operations""" |
475 | + |
476 | + def test_05_api_ready(self): |
477 | + """Test if the API is ready |
478 | + |
479 | + Curl the api endpoint on the sentried graylog unit. We'll retry |
480 | + until the CURL_TIMEOUT because it may take a few seconds for the |
481 | + graylog systemd service to start. |
482 | + """ |
483 | + curl_command = "curl http://localhost:{}".format(self.get_api_port()) |
484 | + timeout = time.time() + CURL_TIMEOUT |
485 | + while time.time() < timeout: |
486 | + response = model.run_on_unit(self.lead_unit_name, curl_command) |
487 | + if response["Code"] == "0": |
488 | + return |
489 | + logging.warning( |
490 | + "Unexpected curl response: {}. Retrying in 30s.".format( |
491 | + response |
492 | + ) |
493 | + ) |
494 | + time.sleep(30) |
495 | + |
496 | + # we didn't get rc=0 in the alloted time, fail the test |
497 | + self.fail( |
498 | + "Graylog didn't respond to the command \n" |
499 | + "'{curl_command}' as expected.\n" |
500 | + "Result: {result}".format( |
501 | + curl_command=curl_command, result=response |
502 | + ) |
503 | + ) |
504 | + |
505 | + def test_06_elasticsearch_healthy(self): |
506 | + resp = self.api.indexer_cluster_health() |
507 | + self.assertEqual(resp["status"], "green") |
508 | + |
509 | + def test_07_elasticsearch_no_faults(self): |
510 | + resp = self.api.indexer_failures_count("2020-01-01") |
511 | + self.assertEqual(int(resp["count"]), 0) |
512 | + |
513 | + def test_08_plugins(self): |
514 | + resp = self.api.request("/system/plugins") |
515 | + plugin_set = set([p["unique_id"] for p in resp["plugins"]]) |
516 | + |
517 | + self.assertTrue(self.expected_plugins <= plugin_set) |
518 | + |
519 | + def test_09_cluster(self): |
520 | + resp = self.api.cluster_get() |
521 | + self.assertGreater(len(resp.values()), 0) |
522 | + node = list(resp.values()).pop() |
523 | + self.assertTrue(node["is_processing"]) |
524 | + self.assertEqual(node["lifecycle"], "running") |
525 | + |
526 | + def test_10_keyword_search(self): |
527 | + params = { |
528 | + "query": '{}: "/var/log/cloud-init-output.log" AND ci-info'.format(self.file_field), |
529 | + "keyword": "4 hours ago", |
530 | + } |
531 | + |
532 | + resp = self.api.request("/search/universal/keyword", params=params) |
533 | + self.assertTrue(int(resp["total_results"]) > 0) |
534 | +>>>>>>> tests/test_graylog_charm.py |
535 | diff --git a/tests/test_legacy.py b/tests/test_legacy.py |
536 | new file mode 100644 |
537 | index 0000000..03294fb |
538 | --- /dev/null |
539 | +++ b/tests/test_legacy.py |
540 | @@ -0,0 +1,83 @@ |
541 | +<<<<<<< tests/test_legacy.py |
542 | +======= |
543 | +import re |
544 | +import unittest |
545 | +import yaml |
546 | +from zaza.utilities import juju |
547 | +from zaza import model |
548 | + |
549 | +from tests.shared import BaseTest, DEFAULT_API_PORT, DEFAULT_WEB_PORT |
550 | + |
551 | + |
552 | +class LegacyTests(unittest.TestCase, BaseTest): |
553 | + |
554 | + # These tests were ported from tests/test_10_basic.py in changeset a3b54c2. |
555 | + # They were temporarily deleted during the conversion from Amulet to Zaza. |
556 | + # Aside from porting to work with libjuju and some refactoring, these tests |
557 | + # are largely as they were before. |
558 | + |
559 | + def test_api_ready(self): |
560 | + """Curl the api endpoint on the graylog unit.""" |
561 | + port = self.get_api_port() |
562 | + curl_command = "curl http://localhost:{} --connect-timeout 30".format(port) |
563 | + self.run_command("graylog/0", curl_command) |
564 | + |
565 | + def test_elasticsearch_active(self): |
566 | + self._assert_remote_address_in_server_conf("elastic/0", "client") |
567 | + # This was also stuck in the same test... |
568 | + api = self.get_graylog_clients()[0] |
569 | + resp = api.indexer_cluster_health() |
570 | + self.assertTrue(resp["status"] == "green") |
571 | + |
572 | + def test_mongodb_active(self): |
573 | + self._assert_remote_address_in_server_conf("mongo/0", "database") |
574 | + # This was also stuck in the same test... |
575 | + api = self.get_graylog_clients()[0] |
576 | + resp = api.cluster_get() |
577 | + self.assertTrue(resp[list(resp)[0]]["is_processing"]) |
578 | + |
579 | + def _assert_remote_address_in_server_conf(self, remote_unit, remote_relation_name): |
580 | + # This is a very simplistic, naive check, but it's equivalent to |
581 | + # that from tests/test_10_basic.py in a3b54c2. |
582 | + source_unit = "graylog/0" |
583 | + remote_addr = juju.get_relation_from_unit( |
584 | + source_unit, remote_unit, remote_relation_name |
585 | + )["private-address"] |
586 | + assert remote_addr in self.get_file_contents( |
587 | + source_unit, "/var/snap/graylog/common/server.conf" |
588 | + ) |
589 | + |
590 | + def test_website_active(self): |
591 | + data = juju.get_relation_from_unit("haproxy/0", "graylog/0", "website") |
592 | + self.assertEqual(data["port"], DEFAULT_WEB_PORT) |
593 | + |
594 | + service_ports = {} |
595 | + for service in yaml.safe_load(data["all_services"]): |
596 | + service_ports[service["service_name"]] = service["service_port"] |
597 | + |
598 | + self.assertEqual(str(service_ports["web"]), DEFAULT_WEB_PORT) |
599 | + if self.is_v2(): |
600 | + self.assertEqual(str(service_ports["api"]), DEFAULT_API_PORT) |
601 | + |
602 | + def run_command(self, target, command, timeout=30): |
603 | + result = model.run_on_unit(target, command, timeout=timeout) |
604 | + assert result["Code"] == "0" |
605 | + return result |
606 | + |
607 | + def test_nrpe_config(self): |
608 | + cfg = self.get_file_contents( |
609 | + "nrpe/0", "/etc/nagios/nrpe.d/check_graylog_health.cfg" |
610 | + ) |
611 | + self.assertTrue(re.search(r"command.*check_graylog_health", cfg)) |
612 | + |
613 | + def test_nrpe_health_check(self): |
614 | + cfg = self.get_file_contents( |
615 | + "nrpe/0", "/usr/local/lib/nagios/plugins/check_graylog_health.py" |
616 | + ) |
617 | + self.assertTrue(re.search(r"CRITICAL", cfg)) |
618 | + |
619 | + def get_file_contents(self, unit, filename): |
620 | + result = self.run_command(unit, "cat '{}'".format(filename)) |
621 | + assert result["Code"] == "0" |
622 | + return result["Stdout"] |
623 | +>>>>>>> tests/test_legacy.py |
624 | diff --git a/tests/test_upgrade.py b/tests/test_upgrade.py |
625 | new file mode 100644 |
626 | index 0000000..49b173a |
627 | --- /dev/null |
628 | +++ b/tests/test_upgrade.py |
629 | @@ -0,0 +1,65 @@ |
630 | +import os |
631 | +import pathlib |
632 | +import subprocess |
633 | +import unittest |
634 | +import time |
635 | +from zaza.charm_lifecycle import utils as zlc_utils |
636 | +from zaza.utilities import juju |
637 | +from zaza import model |
638 | + |
639 | +DEFAULT_API_PORT = "9001" # Graylog 2 only |
640 | +DEFAULT_WEB_PORT = "9000" |
641 | + |
642 | + |
643 | +built_charm = pathlib.Path(os.environ["CHARM_BUILD_DIR"]) / "graylog" |
644 | + |
645 | + |
646 | +class UpgradeTests(unittest.TestCase): |
647 | + |
648 | + """Test for charm upgrading. |
649 | + |
650 | + Intent: if the charm is upgraded from an old version (especially from a |
651 | + GL2-only version to a GL2+3-supporting version), does it indeed still work? |
652 | + |
653 | + """ |
654 | + |
655 | + @classmethod |
656 | + def setUpClass(cls): |
657 | + # 2019-09-26: libjuju doesn't yet support charm upgrades with |
658 | + # --switch <localdir> or --path <localdir>, so we need to fall back to |
659 | + # the command line. |
660 | + model_name = model.get_juju_model() |
661 | + subprocess.check_call([ |
662 | + 'juju', 'upgrade-charm', |
663 | + '--model', model_name, |
664 | + 'old-graylog', '--path', built_charm]) |
665 | + |
666 | + # Ensure we wait enough time for the upgrade to have started. |
667 | + time.sleep(60) |
668 | + |
669 | + test_config = zlc_utils.get_charm_config() |
670 | + model.wait_for_application_states( |
671 | + model_name, |
672 | + test_config.get('target_deploy_status', {})) |
673 | + |
674 | + def test_api_ready(self): |
675 | + """Curl the api endpoint on the graylog unit.""" |
676 | + port = self.get_api_port() |
677 | + curl_command = "curl http://localhost:{} --connect-timeout 30".format(port) |
678 | + self.run_command("graylog/0", curl_command) |
679 | + |
680 | + def get_api_port(self): |
681 | + port = DEFAULT_API_PORT if self.is_v2() else DEFAULT_WEB_PORT |
682 | + return port |
683 | + |
684 | + def run_command(self, target, command, timeout=30): |
685 | + result = model.run_on_unit(target, command, timeout=timeout) |
686 | + assert result["Code"] == "0" |
687 | + return result |
688 | + |
689 | + def is_v2(self): |
690 | + return ( |
691 | + juju.get_full_juju_status() |
692 | + .applications["graylog"]["workload-version"] |
693 | + .startswith("2.") |
694 | + ) |
695 | diff --git a/tests/tests.yaml b/tests/tests.yaml |
696 | new file mode 100644 |
697 | index 0000000..517d340 |
698 | --- /dev/null |
699 | +++ b/tests/tests.yaml |
700 | @@ -0,0 +1,26 @@ |
701 | +<<<<<<< tests/tests.yaml |
702 | +======= |
703 | +charm_name: graylog |
704 | +gate_bundles: |
705 | + - bionic-graylog2 |
706 | + - bionic-graylog3 |
707 | + - focal-graylog2 |
708 | + - focal-graylog3 |
709 | + - bionic-graylog2-ha |
710 | + - bionic-graylog3-ha |
711 | +smoke_bundles: |
712 | + - bionic-graylog3 |
713 | +dev_bundles: |
714 | + - bionic-graylog3 |
715 | +tests: |
716 | + - tests.test_legacy.LegacyTests |
717 | + - tests.test_graylog_charm.CharmOperationTest |
718 | + - tests.test_upgrade.UpgradeTests |
719 | +target_deploy_status: |
720 | + filebeat: |
721 | + workload-status: active |
722 | + workload-status-message: 'Filebeat ready.' |
723 | + haproxy: |
724 | + workload-status: unknown |
725 | + workload-status-message: "" |
726 | +>>>>>>> tests/tests.yaml |
Reworked my pending test code, pulling all of the non-pipeline- related tests together, and rebasing onto a more recent branch.
I have not run tests yet on this, hence still marked as WIP.