Merge ~jfguedez/juju-lint:bug/1903973 into juju-lint: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)
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

To post a comment you must log in.
Revision history for this message
Jose Guedez (jfguedez) wrote :

Add the "metal only" condition for subordinates

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
1diff --git a/contrib/includes/base.yaml b/contrib/includes/base.yaml
2index 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
13diff --git a/jujulint/lint.py b/jujulint/lint.py
14index 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:
110diff --git a/jujulint/util.py b/jujulint/util.py
111index 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)
153diff --git a/tests/conftest.py b/tests/conftest.py
154index 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": {
170diff --git a/tests/test_jujulint.py b/tests/test_jujulint.py
171index 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"]

Subscribers

People subscribed via source and target branches