Merge ~jfguedez/juju-lint:bug/1943222 into juju-lint:master
- Git
- lp:~jfguedez/juju-lint
- bug/1943222
- Merge into master
Proposed by
Jose Guedez
Status: | Merged |
---|---|
Approved by: | James Troup |
Approved revision: | 1e13ff13f087c1f7559cf9550ae0e51909bf9e4e |
Merged at revision: | 76358c531368f9ff72ff4b2772eec7ced0378a2b |
Proposed branch: | ~jfguedez/juju-lint:bug/1943222 |
Merge into: | juju-lint:master |
Diff against target: |
412 lines (+151/-111) 4 files modified
contrib/canonical-rules.yaml (+3/-3) jujulint/lint.py (+126/-103) tests/test_jujulint.py (+20/-5) tox.ini (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Lint maintainers | Pending | ||
Review via email: mp+408463@code.launchpad.net |
Commit message
Add 'neq' check for charm configuration
Description of the change
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 76358c531368f9f
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/contrib/canonical-rules.yaml b/contrib/canonical-rules.yaml |
2 | index fcdfdaa..556c84d 100644 |
3 | --- a/contrib/canonical-rules.yaml |
4 | +++ b/contrib/canonical-rules.yaml |
5 | @@ -24,7 +24,7 @@ openstack config: |
6 | eq: 9000 |
7 | nova-compute: |
8 | cpu-model: |
9 | - isset: true |
10 | + neq: "" |
11 | ceph-osd-replication-count: |
12 | eq: 3 |
13 | percona-cluster: |
14 | @@ -59,9 +59,9 @@ config: |
15 | eq: false |
16 | nrpe: |
17 | lacp_bonds: |
18 | - isset: true |
19 | + neq: "" |
20 | netlinks: |
21 | - isset: true |
22 | + neq: "" |
23 | landscape-client: |
24 | disable-unattended-upgrades: |
25 | eq: true |
26 | diff --git a/jujulint/lint.py b/jujulint/lint.py |
27 | index a8ae702..56a11b6 100755 |
28 | --- a/jujulint/lint.py |
29 | +++ b/jujulint/lint.py |
30 | @@ -32,12 +32,29 @@ import attr |
31 | from jujulint.util import flatten_list, is_container, extract_charm_name |
32 | from jujulint.logging import Logger |
33 | |
34 | +VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte") |
35 | + |
36 | +# Generic named tuple to represent the binary config operators (eq,neq,gte) |
37 | +ConfigOperator = collections.namedtuple( |
38 | + "ConfigOperator", "name repr check error_template" |
39 | +) |
40 | + |
41 | # TODO: |
42 | -# - tests |
43 | # - missing relations for mandatory subordinates |
44 | # - info mode, e.g. num of machines, version (e.g. look at ceph), architecture |
45 | |
46 | |
47 | +def helper_operator_eq_check(check_value, actual_value): |
48 | + """Perform the actual equality check for the eq/neq rules.""" |
49 | + match = False |
50 | + try: |
51 | + match = re.match(re.compile(str(check_value)), str(actual_value)) |
52 | + except re.error: |
53 | + match = check_value == actual_value |
54 | + |
55 | + return match |
56 | + |
57 | + |
58 | @attrs |
59 | class ModelInfo(object): |
60 | """Represent information obtained from juju status data.""" |
61 | @@ -183,59 +200,12 @@ class Linter: |
62 | |
63 | return _int * conv[val[-1].lower()] |
64 | |
65 | - def gte(self, name, check_value, rule, config): |
66 | - """Check if value is greater than or equal to the check value.""" |
67 | - if rule in config: |
68 | - current = self.atoi(config[rule]) |
69 | - expected = self.atoi(check_value) |
70 | - if current >= expected: |
71 | - self.logger.debug( |
72 | - "[{}] [{}/{}] (PASS) Application {} has config for {} which is >= {}: {}.".format( |
73 | - self.cloud_name, |
74 | - self.controller_name, |
75 | - self.model_name, |
76 | - name, |
77 | - rule, |
78 | - check_value, |
79 | - config[rule], |
80 | - ) |
81 | - ) |
82 | - return True |
83 | - |
84 | - actual_value = config[rule] |
85 | - self.handle_error( |
86 | - { |
87 | - "id": "config-gte-check", |
88 | - "tags": ["config", "gte"], |
89 | - "description": "Checks for config condition 'gte'", |
90 | - "application": name, |
91 | - "rule": rule, |
92 | - "expected_value": check_value, |
93 | - "actual_value": actual_value, |
94 | - "message": "Application {} has config for {} which is less than {}: {}.".format( |
95 | - name, rule, check_value, actual_value |
96 | - ), |
97 | - } |
98 | - ) |
99 | - return False |
100 | - self.logger.warn( |
101 | - "[{}] [{}/{}] When checking if application {} has no config for {}, can't determine if >= than {}.".format( |
102 | - self.cloud_name, |
103 | - self.controller_name, |
104 | - self.model_name, |
105 | - name, |
106 | - rule, |
107 | - check_value, |
108 | - ) |
109 | - ) |
110 | - return False |
111 | - |
112 | def isset(self, name, check_value, rule, config): |
113 | """Check if value is set per rule constraints.""" |
114 | if rule in config: |
115 | if check_value is True: |
116 | self.logger.debug( |
117 | - "[{}] [{}/{}] (PASS) Application {} correctly has manual config for {}: {}.".format( |
118 | + "[{}] [{}/{}] (PASS) Application {} correctly has config for '{}': {}.".format( |
119 | self.cloud_name, |
120 | self.controller_name, |
121 | self.model_name, |
122 | @@ -254,15 +224,15 @@ class Linter: |
123 | "application": name, |
124 | "rule": rule, |
125 | "actual_value": actual_value, |
126 | - "message": "Application {} has manual config for {}: {}.".format( |
127 | + "message": "Application {} has config for {}: {}.".format( |
128 | name, rule, actual_value |
129 | ), |
130 | } |
131 | ) |
132 | return False |
133 | - if check_value is False: |
134 | + elif check_value is False: |
135 | self.logger.debug( |
136 | - "[{}] [{}/{}] (PASS) Application {} is correctly using default config for {}.".format( |
137 | + "[{}] [{}/{}] (PASS) Application {} correctly had no config for '{}'.".format( |
138 | self.cloud_name, |
139 | self.controller_name, |
140 | self.model_name, |
141 | @@ -278,61 +248,118 @@ class Linter: |
142 | "description": "Checks for config condition 'isset' true", |
143 | "application": name, |
144 | "rule": rule, |
145 | - "message": "Application {} has no manual config for {}.".format( |
146 | - name, rule |
147 | - ), |
148 | + "message": "Application {} has no config for {}.".format(name, rule), |
149 | } |
150 | ) |
151 | return False |
152 | |
153 | - def eq(self, name, check_value, rule, config): |
154 | - """Check if value is matches the provided value or regex, autodetecting regex.""" |
155 | - if rule in config: |
156 | - match = False |
157 | - try: |
158 | - match = re.match(re.compile(str(check_value)), str(config[rule])) |
159 | - except re.error: |
160 | - match = check_value == config[rule] |
161 | - if match: |
162 | - self.logger.debug( |
163 | - "[{}] [{}/{}] Application {} has correct setting for {}: Expected {}, got {}.".format( |
164 | - self.cloud_name, |
165 | - self.controller_name, |
166 | - self.model_name, |
167 | - name, |
168 | - rule, |
169 | - check_value, |
170 | - config[rule], |
171 | - ) |
172 | + def eq(self, app_name, check_value, config_key, app_config): |
173 | + """Check if value matches the provided value or regex, autodetecting regex.""" |
174 | + operator = ConfigOperator( |
175 | + name="eq", |
176 | + repr="==", |
177 | + check=helper_operator_eq_check, |
178 | + error_template="Application {} has incorrect setting for '{}': Expected {}, got {}", |
179 | + ) |
180 | + |
181 | + return self.check_config_generic( |
182 | + operator, app_name, check_value, config_key, app_config |
183 | + ) |
184 | + |
185 | + def neq(self, app_name, check_value, config_key, app_config): |
186 | + """Check if value does not match a the config.""" |
187 | + operator = ConfigOperator( |
188 | + name="neq", |
189 | + repr="!=", |
190 | + check=lambda check_value, actual_value: not helper_operator_eq_check( |
191 | + check_value, actual_value |
192 | + ), |
193 | + error_template="Application {} has incorrect setting for '{}': Should not be {}", |
194 | + ) |
195 | + |
196 | + return self.check_config_generic( |
197 | + operator, app_name, check_value, config_key, app_config |
198 | + ) |
199 | + |
200 | + def gte(self, app_name, check_value, config_key, app_config): |
201 | + """Check if value is greater than or equal to the check value.""" |
202 | + |
203 | + def operator_gte_check(check_value, actual_value): |
204 | + """Perform the actual gte check.""" |
205 | + current = self.atoi(actual_value) |
206 | + expected = self.atoi(check_value) |
207 | + return current >= expected |
208 | + |
209 | + operator = ConfigOperator( |
210 | + name="gte", |
211 | + repr=">=", |
212 | + check=operator_gte_check, |
213 | + error_template="Application {} has config for '{}' which is less than {}: {}", |
214 | + ) |
215 | + |
216 | + return self.check_config_generic( |
217 | + operator, app_name, check_value, config_key, app_config |
218 | + ) |
219 | + |
220 | + def check_config_generic( |
221 | + self, operator, app_name, check_value, config_key, app_config |
222 | + ): |
223 | + """Apply the provided config operator to the configuration.""" |
224 | + model_header = "[{}] [{}/{}]".format( |
225 | + self.cloud_name, |
226 | + self.controller_name, |
227 | + self.model_name, |
228 | + ) |
229 | + |
230 | + # First check if the config key is present |
231 | + if config_key not in app_config: |
232 | + self.logger.warn( |
233 | + "{} Application {} has no config for '{}', cannot determine if {} {}.".format( |
234 | + model_header, |
235 | + app_name, |
236 | + config_key, |
237 | + operator.repr, |
238 | + repr(check_value), |
239 | ) |
240 | - return True |
241 | - actual_value = config[rule] |
242 | + ) |
243 | + return False |
244 | + |
245 | + actual_value = app_config[config_key] |
246 | + |
247 | + # Apply the check callable and handle the possible cases |
248 | + if operator.check(check_value, actual_value): |
249 | + self.logger.debug( |
250 | + "{} Application {} has a valid config for '{}': {} ({} {})".format( |
251 | + model_header, |
252 | + app_name, |
253 | + config_key, |
254 | + repr(check_value), |
255 | + operator.repr, |
256 | + repr(actual_value), |
257 | + ) |
258 | + ) |
259 | + return True |
260 | + else: |
261 | self.handle_error( |
262 | { |
263 | - "id": "config-eq-check", |
264 | - "tags": ["config", "eq"], |
265 | - "description": "Checks for config condition 'eq'", |
266 | - "application": name, |
267 | - "rule": rule, |
268 | + "id": "config-{}-check".format(operator.name), |
269 | + "tags": ["config", operator.name], |
270 | + "description": "Checks for config condition '{}'".format( |
271 | + operator.name |
272 | + ), |
273 | + "application": app_name, |
274 | + "rule": config_key, |
275 | "expected_value": check_value, |
276 | "actual_value": actual_value, |
277 | - "message": "Application {} has incorrect setting for {}: Expected {}, got {}.".format( |
278 | - name, rule, check_value, actual_value |
279 | + "message": operator.error_template.format( |
280 | + app_name, |
281 | + config_key, |
282 | + repr(check_value), |
283 | + repr(actual_value), |
284 | ), |
285 | } |
286 | ) |
287 | - return False |
288 | - else: |
289 | - self.logger.warn( |
290 | - "[{}] [{}/{}] When checking if application {} has no config for {}, can't determine if == {}.".format( |
291 | - self.cloud_name, |
292 | - self.controller_name, |
293 | - self.model_name, |
294 | - name, |
295 | - rule, |
296 | - check_value, |
297 | - ) |
298 | - ) |
299 | + return False |
300 | |
301 | def check_config(self, name, config, rules): |
302 | """Check application against provided rules.""" |
303 | @@ -344,12 +371,10 @@ class Linter: |
304 | ) |
305 | ) |
306 | for check_op, check_value in rules[rule].items(): |
307 | - if check_op == "isset": |
308 | - self.isset(name, check_value, rule, config) |
309 | - elif check_op == "eq": |
310 | - self.eq(name, check_value, rule, config) |
311 | - elif check_op == "gte": |
312 | - self.gte(name, check_value, rule, config) |
313 | + # check_op should be the operator name, e.g. (eq, neq, gte, isset) |
314 | + if check_op in VALID_CONFIG_CHECKS: |
315 | + check_method = getattr(self, check_op) |
316 | + check_method(name, check_value, rule, config) |
317 | else: |
318 | self.logger.warn( |
319 | "[{}] [{}/{}] Application {} has unknown check operation for {}: {}.".format( |
320 | @@ -740,7 +765,6 @@ class Linter: |
321 | } |
322 | ) |
323 | |
324 | - |
325 | def results(self): |
326 | """Provide results of the linting process.""" |
327 | if self.model.missing_subs: |
328 | @@ -1080,7 +1104,6 @@ class Linter: |
329 | |
330 | def handle_error(self, error): |
331 | """Collect an error and add it to the collector.""" |
332 | - |
333 | self.logger.error( |
334 | "[{}] [{}/{}] {}.".format( |
335 | self.cloud_name, self.controller_name, self.model_name, error["message"] |
336 | diff --git a/tests/test_jujulint.py b/tests/test_jujulint.py |
337 | index 35c4bbe..6aa6ab4 100644 |
338 | --- a/tests/test_jujulint.py |
339 | +++ b/tests/test_jujulint.py |
340 | @@ -2,7 +2,6 @@ |
341 | """Tests for jujulint.""" |
342 | |
343 | import pytest |
344 | -import jujulint |
345 | |
346 | |
347 | def test_flatten_list(utils): |
348 | @@ -37,6 +36,8 @@ def test_map_charms(linter, utils): |
349 | |
350 | |
351 | class TestLinter: |
352 | + """Test the main Linter class.""" |
353 | + |
354 | def test_minimal_rules(self, linter, juju_status): |
355 | """Test that the base rules/status have no errors.""" |
356 | linter.do_lint(juju_status) |
357 | @@ -90,7 +91,7 @@ class TestLinter: |
358 | assert errors[0]["status_since"] == "01 Apr 2021 05:14:13Z" |
359 | assert errors[0]["status_msg"] == 'hook failed: "install"' |
360 | |
361 | - def test_AZ_parsing(self, linter, juju_status): |
362 | + def test_az_parsing(self, linter, juju_status): |
363 | """Test that the AZ parsing is working as expected.""" |
364 | # duplicate a AZ name so we have 2 AZs instead of the expected 3 |
365 | juju_status["machines"]["2"]["hardware"] = "availability-zone=rack-1" |
366 | @@ -101,7 +102,7 @@ class TestLinter: |
367 | assert errors[0]["id"] == "AZ-invalid-number" |
368 | assert errors[0]["num_azs"] == 2 |
369 | |
370 | - def test_AZ_balancing(self, linter, juju_status): |
371 | + def test_az_balancing(self, linter, juju_status): |
372 | """Test that applications are balanced across AZs.""" |
373 | # add an extra machine in an existing AZ |
374 | juju_status["machines"].update( |
375 | @@ -277,8 +278,22 @@ class TestLinter: |
376 | assert errors[0]["id"] == "config-eq-check" |
377 | assert errors[0]["application"] == "ubuntu" |
378 | assert errors[0]["rule"] == "fake-opt" |
379 | - assert errors[0]["expected_value"] == False |
380 | - assert errors[0]["actual_value"] == True |
381 | + assert errors[0]["expected_value"] is False |
382 | + assert errors[0]["actual_value"] is True |
383 | + |
384 | + def test_config_neq(self, linter, juju_status): |
385 | + """Test the config condition 'neq'.""" |
386 | + linter.lint_rules["config"] = {"ubuntu": {"fake-opt": {"neq": False}}} |
387 | + juju_status["applications"]["ubuntu"]["options"] = {"fake-opt": False} |
388 | + linter.do_lint(juju_status) |
389 | + |
390 | + errors = linter.output_collector["errors"] |
391 | + assert len(errors) == 1 |
392 | + assert errors[0]["id"] == "config-neq-check" |
393 | + assert errors[0]["application"] == "ubuntu" |
394 | + assert errors[0]["rule"] == "fake-opt" |
395 | + assert errors[0]["expected_value"] is False |
396 | + assert errors[0]["actual_value"] is False |
397 | |
398 | def test_config_gte(self, linter, juju_status): |
399 | """Test the config condition 'gte'.""" |
400 | diff --git a/tox.ini b/tox.ini |
401 | index cf22f0f..3e9dbdf 100644 |
402 | --- a/tox.ini |
403 | +++ b/tox.ini |
404 | @@ -3,6 +3,8 @@ exclude = |
405 | .git, |
406 | __pycache__, |
407 | .tox, |
408 | + .eggs, |
409 | + venv, |
410 | max-line-length = 120 |
411 | max-complexity = 10 |
412 | ignore = C901 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.