Merge ~jfguedez/juju-lint:bug/1903973 into juju-lint:master
- Git
- lp:~jfguedez/juju-lint
- bug/1903973
- Merge into master
Proposed by
Jose Guedez
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 3b85de9d81bf29c7769febd0a60c7a41fc424a34 | ||||
Proposed branch: | ~jfguedez/juju-lint:bug/1903973 | ||||
Merge into: | juju-lint:master | ||||
Diff against target: |
340 lines (+158/-47) 5 files modified
contrib/includes/base.yaml (+1/-1) jujulint/lint.py (+22/-8) jujulint/util.py (+25/-3) tests/conftest.py (+6/-0) tests/test_jujulint.py (+104/-35) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Edin S (community) | Approve | ||
Juju Lint maintainers | Pending | ||
Review via email: mp+408853@code.launchpad.net |
Commit message
Add the "metal only" condition for subordinates
Description of the change
To post a comment you must log in.
Revision history for this message
Jose Guedez (jfguedez) wrote : | # |
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
Revision history for this message
Edin S (exsdev) : | # |
review:
Approve
Revision history for this message
James Troup (elmo) wrote : | # |
I had to thought check MAC addresses, FWIW; it's also a heuristic, but a fairly reliable one for detecting the most common virt platforms (KVM/VMWare).
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/contrib/includes/base.yaml b/contrib/includes/base.yaml |
2 | index 1815ee2..da558a3 100644 |
3 | --- a/contrib/includes/base.yaml |
4 | +++ b/contrib/includes/base.yaml |
5 | @@ -35,6 +35,6 @@ subordinates: |
6 | thruk-agent: |
7 | where: on nagios |
8 | hw-health: |
9 | - where: host only |
10 | + where: metal only |
11 | logrotated: |
12 | where: all |
13 | diff --git a/jujulint/lint.py b/jujulint/lint.py |
14 | index 68fef78..f680370 100755 |
15 | --- a/jujulint/lint.py |
16 | +++ b/jujulint/lint.py |
17 | @@ -33,7 +33,7 @@ import attr |
18 | import dateutil.parser |
19 | from dateutil import relativedelta |
20 | |
21 | -from jujulint.util import flatten_list, is_container, extract_charm_name |
22 | +import jujulint.util as utils |
23 | from jujulint.logging import Logger |
24 | |
25 | VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte") |
26 | @@ -133,7 +133,9 @@ class Linter: |
27 | self.lint_rules["subordinates"][name] = dict(where=where) |
28 | |
29 | # Flatten all entries (to account for nesting due to YAML anchors (templating) |
30 | - self.lint_rules = {k: flatten_list(v) for k, v in self.lint_rules.items()} |
31 | + self.lint_rules = { |
32 | + k: utils.flatten_list(v) for k, v in self.lint_rules.items() |
33 | + } |
34 | |
35 | self._log_with_header( |
36 | "Lint Rules: {}".format(pprint.pformat(self.lint_rules)) |
37 | @@ -372,7 +374,7 @@ class Linter: |
38 | ) |
39 | continue |
40 | |
41 | - charm_name = extract_charm_name(applications[application]["charm"]) |
42 | + charm_name = utils.extract_charm_name(applications[application]["charm"]) |
43 | if "config" in self.lint_rules: |
44 | if charm_name in self.lint_rules["config"]: |
45 | lint_rules = self.lint_rules["config"][charm_name].items() |
46 | @@ -393,7 +395,7 @@ class Linter: |
47 | lint_rules, |
48 | ) |
49 | |
50 | - def check_subs(self): |
51 | + def check_subs(self, machines_data): |
52 | """Check the subordinates in the model.""" |
53 | all_or_nothing = set() |
54 | for machine in self.model.subs_on_machines: |
55 | @@ -429,7 +431,7 @@ class Linter: |
56 | self._log_with_header( |
57 | "requirement is 'host only' form...." |
58 | ) |
59 | - if is_container(machine): |
60 | + if utils.is_container(machine): |
61 | self._log_with_header("... and we are a container, checking") |
62 | # XXX check alternate names? |
63 | if required_sub in present_subs: |
64 | @@ -438,6 +440,18 @@ class Linter: |
65 | self.model.extraneous_subs[required_sub].add(app) |
66 | continue |
67 | self._log_with_header("... and we are a host, will fallthrough") |
68 | + elif where == "metal only": |
69 | + self._log_with_header( |
70 | + "requirement is 'metal only' form...." |
71 | + ) |
72 | + if not utils.is_metal(machine, machines_data.get(machine, {})): |
73 | + self._log_with_header("... and we are not a metal, checking") |
74 | + if required_sub in present_subs: |
75 | + self._log_with_header("... found extraneous sub") |
76 | + for app in self.model.apps_on_machines[machine]: |
77 | + self.model.extraneous_subs[required_sub].add(app) |
78 | + continue |
79 | + self._log_with_header("... and we are a metal, will fallthrough") |
80 | |
81 | elif where == "all or nothing" and required_sub not in all_or_nothing: |
82 | self._log_with_header( |
83 | @@ -448,7 +462,7 @@ class Linter: |
84 | # need to change the name we expect to see it as |
85 | elif where == "container aware": |
86 | self._log_with_header("requirement is 'container aware'.") |
87 | - if is_container(machine): |
88 | + if utils.is_container(machine): |
89 | suffixes = self.lint_rules["subordinates"][required_sub][ |
90 | "container-suffixes" |
91 | ] |
92 | @@ -719,7 +733,7 @@ class Linter: |
93 | """Process applications in the model, validating and normalising the names.""" |
94 | for app in applications: |
95 | if "charm" in applications[app]: |
96 | - charm_name = extract_charm_name(applications[app]["charm"]) |
97 | + charm_name = utils.extract_charm_name(applications[app]["charm"]) |
98 | self.model.charms.add(charm_name) |
99 | self.model.app_to_charm[app] = charm_name |
100 | else: |
101 | @@ -973,7 +987,7 @@ class Linter: |
102 | for app in parsed_yaml[applications]: |
103 | self.process_subordinates(parsed_yaml[applications][app], app) |
104 | |
105 | - self.check_subs() |
106 | + self.check_subs(parsed_yaml["machines"]) |
107 | self.check_charms() |
108 | |
109 | if "relations" not in parsed_yaml: |
110 | diff --git a/jujulint/util.py b/jujulint/util.py |
111 | index c6eb80e..fc45ee6 100644 |
112 | --- a/jujulint/util.py |
113 | +++ b/jujulint/util.py |
114 | @@ -50,13 +50,35 @@ def is_container(machine): |
115 | return False |
116 | |
117 | |
118 | +def is_virtual_machine(machine, machine_data): |
119 | + """ |
120 | + Check if a provided machine is a VM. |
121 | + |
122 | + It is not straightforward to determine if a machine is a VM from juju data |
123 | + (bundle/juju status). In some cases a "hardware" key is provided (jsfy), |
124 | + and in those cases we can check for the keyword "virtual" since some |
125 | + provisioners include a tag there (FCE). We use that criteria as a best |
126 | + effort attempt to determine if the machine is a VM. |
127 | + """ |
128 | + hardware = machine_data.get("hardware") |
129 | + return bool(hardware and "virtual" in hardware) |
130 | + |
131 | + |
132 | +def is_metal(machine, machine_data): |
133 | + """ |
134 | + Check if a provided machine is a bare metal host. |
135 | + |
136 | + Leverages the other detection methods, if the others fail (e.g. not a |
137 | + container or VM), we consider the machine to be bare metal. |
138 | + """ |
139 | + return not (is_container(machine) or is_virtual_machine(machine, machine_data)) |
140 | + |
141 | + |
142 | def extract_charm_name(charm): |
143 | """Extract the charm name using regex.""" |
144 | match = re.match( |
145 | r"^(?:\w+:)?(?:~[\w\.-]+/)?(?:\w+/)?([a-zA-Z0-9-]+?)(?:-\d+)?$", charm |
146 | ) |
147 | if not match: |
148 | - raise InvalidCharmNameError( |
149 | - "charm name '{}' is invalid".format(charm) |
150 | - ) |
151 | + raise InvalidCharmNameError("charm name '{}' is invalid".format(charm)) |
152 | return match.group(1) |
153 | diff --git a/tests/conftest.py b/tests/conftest.py |
154 | index ba9ed77..fc7093e 100644 |
155 | --- a/tests/conftest.py |
156 | +++ b/tests/conftest.py |
157 | @@ -98,6 +98,12 @@ def juju_status(): |
158 | } |
159 | }, |
160 | }, |
161 | + "ntp": { |
162 | + "application-status": {"current": "active"}, |
163 | + "charm": "cs:ntp-47", |
164 | + "charm-name": "ntp", |
165 | + "relations": {"juju-info": ["ubuntu"]}, |
166 | + }, |
167 | }, |
168 | "machines": { |
169 | "0": { |
170 | diff --git a/tests/test_jujulint.py b/tests/test_jujulint.py |
171 | index ce123ee..2494c28 100644 |
172 | --- a/tests/test_jujulint.py |
173 | +++ b/tests/test_jujulint.py |
174 | @@ -5,41 +5,59 @@ from datetime import datetime, timezone |
175 | import pytest |
176 | |
177 | |
178 | -def test_flatten_list(utils): |
179 | - """Test the utils flatten_list function.""" |
180 | - unflattened_list = [1, [2, 3]] |
181 | - flattened_list = [1, 2, 3] |
182 | - assert flattened_list == utils.flatten_list(unflattened_list) |
183 | - |
184 | - unflattened_list = [1, [2, [3, 4]]] |
185 | - flattened_list = [1, 2, 3, 4] |
186 | - assert flattened_list == utils.flatten_list(unflattened_list) |
187 | - |
188 | - |
189 | -def test_flatten_list_non_list_iterable(utils): |
190 | - """Test the utils flatten_list function.""" |
191 | - iterable = {1: 2} |
192 | - assert iterable == utils.flatten_list(iterable) |
193 | - |
194 | - |
195 | -def test_map_charms(linter, utils): |
196 | - """Test the charm name validation code.""" |
197 | - applications = { |
198 | - "test-app-1": {"charm": "cs:~USER/SERIES/TEST-CHARM12-123"}, |
199 | - "test-app-2": {"charm": "cs:~USER/TEST-CHARM12-123"}, |
200 | - "test-app-3": {"charm": "cs:TEST-CHARM12-123"}, |
201 | - "test-app-4": {"charm": "local:SERIES/TEST-CHARM12"}, |
202 | - "test-app-5": {"charm": "local:TEST-CHARM12"}, |
203 | - "test-app-6": {"charm": "cs:~TEST-CHARMERS/TEST-CHARM12-123"}, |
204 | - } |
205 | - linter.map_charms(applications) |
206 | - for charm in linter.model.charms: |
207 | - assert "TEST-CHARM12" == charm |
208 | - applications = { |
209 | - "test-app1": {"charm": "cs:invalid-charm$"}, |
210 | - } |
211 | - with pytest.raises(utils.InvalidCharmNameError): |
212 | - linter.map_charms(applications) |
213 | +class TestUtils: |
214 | + """Test the jujulint utilities.""" |
215 | + |
216 | + def test_flatten_list(self, utils): |
217 | + """Test the utils flatten_list function.""" |
218 | + unflattened_list = [1, [2, 3]] |
219 | + flattened_list = [1, 2, 3] |
220 | + assert flattened_list == utils.flatten_list(unflattened_list) |
221 | + |
222 | + unflattened_list = [1, [2, [3, 4]]] |
223 | + flattened_list = [1, 2, 3, 4] |
224 | + assert flattened_list == utils.flatten_list(unflattened_list) |
225 | + |
226 | + def test_flatten_list_non_list_iterable(self, utils): |
227 | + """Test the utils flatten_list function.""" |
228 | + iterable = {1: 2} |
229 | + assert iterable == utils.flatten_list(iterable) |
230 | + |
231 | + def test_is_container(self, utils): |
232 | + """Test the utils is_container function.""" |
233 | + assert utils.is_container("1/lxd/0") is True |
234 | + assert utils.is_container("0") is False |
235 | + |
236 | + def test_is_virtual_machine(self, utils): |
237 | + """Test the utils is_virtual_machine function.""" |
238 | + machine = "0" |
239 | + machine_data = { |
240 | + "hardware": "arch=amd64 cores=2 mem=4096M tags=virtual,pod-console-logging,vault availability-zone=AZ3" |
241 | + } |
242 | + assert utils.is_virtual_machine(machine, machine_data) is True |
243 | + |
244 | + machine_data = {} |
245 | + assert utils.is_virtual_machine(machine, machine_data) is False |
246 | + |
247 | + def test_is_metal(self, utils): |
248 | + """Test the utils is_metal function.""" |
249 | + # A VM should return false |
250 | + machine = "0" |
251 | + machine_data = { |
252 | + "hardware": "arch=amd64 cores=2 mem=4096M tags=virtual,pod-console-logging,vault availability-zone=AZ3" |
253 | + } |
254 | + assert utils.is_metal(machine, machine_data) is False |
255 | + |
256 | + # A container should return false |
257 | + assert utils.is_metal("1/lxd/0", {}) is False |
258 | + |
259 | + # A bare metal should return true |
260 | + machine = "1" |
261 | + machine_data = { |
262 | + "hardware": "arch=amd64 cores=128 mem=2093056M tags=foundation-nodes,hyper-converged-az2 " |
263 | + "availability-zone=AZ2" |
264 | + } |
265 | + assert utils.is_metal(machine, machine_data) is True |
266 | |
267 | |
268 | class TestLinter: |
269 | @@ -77,6 +95,25 @@ class TestLinter: |
270 | assert errors[0]["id"] == "charm-not-mapped" |
271 | assert errors[0]["application"] == "ubuntu2" |
272 | |
273 | + def test_map_charms(self, linter, utils): |
274 | + """Test the charm name validation code.""" |
275 | + applications = { |
276 | + "test-app-1": {"charm": "cs:~USER/SERIES/TEST-CHARM12-123"}, |
277 | + "test-app-2": {"charm": "cs:~USER/TEST-CHARM12-123"}, |
278 | + "test-app-3": {"charm": "cs:TEST-CHARM12-123"}, |
279 | + "test-app-4": {"charm": "local:SERIES/TEST-CHARM12"}, |
280 | + "test-app-5": {"charm": "local:TEST-CHARM12"}, |
281 | + "test-app-6": {"charm": "cs:~TEST-CHARMERS/TEST-CHARM12-123"}, |
282 | + } |
283 | + linter.map_charms(applications) |
284 | + for charm in linter.model.charms: |
285 | + assert "TEST-CHARM12" == charm |
286 | + applications = { |
287 | + "test-app1": {"charm": "cs:invalid-charm$"}, |
288 | + } |
289 | + with pytest.raises(utils.InvalidCharmNameError): |
290 | + linter.map_charms(applications) |
291 | + |
292 | def test_juju_status_unexpected(self, linter, juju_status): |
293 | """Test that juju and workload status is expected.""" |
294 | # inject invalid status to the application |
295 | @@ -243,6 +280,37 @@ class TestLinter: |
296 | assert errors[0]["machines"] == "0" |
297 | assert errors[0]["subordinate"] == "ntp" |
298 | |
299 | + def test_ops_subordinate_metal_only1(self, linter, juju_status): |
300 | + """ |
301 | + Test that missing ops subordinate charms are detected. |
302 | + |
303 | + Use the "metal only" rule in a bare metal machine, should report the |
304 | + missing subordinate |
305 | + """ |
306 | + linter.lint_rules["subordinates"]["hw-health"] = {"where": "metal only"} |
307 | + linter.do_lint(juju_status) |
308 | + |
309 | + errors = linter.output_collector["errors"] |
310 | + assert len(errors) == 1 |
311 | + assert errors[0]["id"] == "ops-subordinate-missing" |
312 | + assert errors[0]["principals"] == "ubuntu" |
313 | + assert errors[0]["subordinate"] == "hw-health" |
314 | + |
315 | + def test_ops_subordinate_metal_only2(self, linter, juju_status): |
316 | + """ |
317 | + Test that missing ops subordinate charms are detected. |
318 | + |
319 | + Use the "metal only" rule in a VM, should ignore it |
320 | + """ |
321 | + linter.lint_rules["subordinates"]["hw-health"] = {"where": "metal only"} |
322 | + |
323 | + # Turn machine "0" into a "VM" |
324 | + juju_status["machines"]["0"]["hardware"] = "tags=virtual availability-zone=rack-1" |
325 | + linter.do_lint(juju_status) |
326 | + |
327 | + errors = linter.output_collector["errors"] |
328 | + assert not errors |
329 | + |
330 | def test_openstack_charm_missing(self, linter, juju_status): |
331 | """Test that missing openstack mandatory charms are detected.""" |
332 | linter.cloud_type = "openstack" |
333 | @@ -286,6 +354,7 @@ class TestLinter: |
334 | linter.cloud_type = "kubernetes" |
335 | linter.lint_rules["kubernetes mandatory"] = [] |
336 | linter.lint_rules["operations kubernetes mandatory"] = ["ntp"] |
337 | + juju_status["applications"].pop("ntp") # drop the app from the model |
338 | linter.do_lint(juju_status) |
339 | |
340 | errors = linter.output_collector["errors"] |
Add the "metal only" condition for subordinates