Merge ~gabrielcocenza/juju-lint:bug/1990885 into juju-lint:master

Proposed by Gabriel Cocenza
Status: Merged
Approved by: Eric Chen
Approved revision: d11c04e32c8c1d34e3f58ade4bf6c6297686773d
Merged at revision: 52fe9bef72b5aec36de0f320e32c994d8fbd96ed
Proposed branch: ~gabrielcocenza/juju-lint:bug/1990885
Merge into: juju-lint:master
Diff against target: 1083 lines (+661/-46)
10 files modified
jujulint/checks/hyper_converged.py (+38/-0)
jujulint/lint.py (+31/-2)
jujulint/model_input.py (+68/-3)
tests/unit/conftest.py (+337/-1)
tests/unit/test_hyper_converged.py (+32/-0)
tests/unit/test_input.py (+61/-0)
tests/unit/test_jujulint.py (+62/-4)
tests/unit/test_relations.py (+2/-2)
tests/unit/test_spaces.py (+29/-33)
tox.ini (+1/-1)
Reviewer Review Type Date Requested Status
Eric Chen Approve
🤖 prod-jenkaas-bootstack continuous-integration Approve
Mert Kirpici (community) Approve
JamesLin Approve
Review via email: mp+431602@code.launchpad.net

Commit message

added check for hyper converged deployments

- warning for hyper converged deployments with Masakari
- added machines_to_apps field, filter_machines_by_charm
  and filter_lxd_on_machine methods on model_input.
- created subdirectory form checks
- renamed module check_spaces.py to spaces.py

Closes-Bug: #1990885

To post a comment you must log in.
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
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
JamesLin (jneo8) wrote :

LGTM

review: Approve
Revision history for this message
Mert Kirpici (mertkirpici) wrote :

thanks Gabriel. I proposed one suggestion inline.

review: Approve
Revision history for this message
Eric Chen (eric-chen) wrote :

Agree with Mert, please change it before we merge it. thanks!

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Eric Chen (eric-chen) :
review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 52fe9bef72b5aec36de0f320e32c994d8fbd96ed

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/jujulint/checks/hyper_converged.py b/jujulint/checks/hyper_converged.py
2new file mode 100644
3index 0000000..0dd5da6
4--- /dev/null
5+++ b/jujulint/checks/hyper_converged.py
6@@ -0,0 +1,38 @@
7+#!/usr/bin/python3
8+"""Checks if nodes can be Hyper-Converged."""
9+
10+from collections import defaultdict
11+from typing import DefaultDict, Union
12+
13+from jujulint.model_input import JujuBundleFile, JujuStatusFile
14+
15+
16+# see LP#1990885
17+def check_hyper_converged(
18+ input_file: Union[JujuBundleFile, JujuStatusFile]
19+) -> DefaultDict[str, DefaultDict[str, set]]:
20+ """Check if other services are collocated with nova/osd with masakari.
21+
22+ Hyperconvered is nova/osd collocated with openstack services.
23+ Masakari uses ha-cluster to monitor nodes. If the node is not responsive then the
24+ node is taken down. This is fine for nova/osd units, but if there are collocated
25+ with openstack services this can be problematic.
26+
27+
28+ :param input_file: mapped content of the input file.
29+ :type input_file: Union[JujuBundleFile, JujuStatusFile]
30+ :return: Services on lxds that are on nova/osd machines.
31+ :rtype: DefaultDict[str, DefaultDict[str, set]]
32+ """
33+ hyper_converged_warning = defaultdict(lambda: defaultdict(set))
34+ if "masakari" in input_file.charms:
35+ nova_machines = input_file.filter_machines_by_charm("nova-compute")
36+ ods_machines = input_file.filter_machines_by_charm("ceph-osd")
37+ nova_osd_machines = nova_machines.intersection(ods_machines)
38+ if nova_osd_machines:
39+ for machine in nova_osd_machines:
40+ lxds = input_file.filter_lxd_on_machine(machine)
41+ for lxd in lxds:
42+ apps = input_file.machines_to_apps[lxd]
43+ hyper_converged_warning[machine][lxd] = apps
44+ return hyper_converged_warning
45diff --git a/jujulint/relations.py b/jujulint/checks/relations.py
46similarity index 100%
47rename from jujulint/relations.py
48rename to jujulint/checks/relations.py
49diff --git a/jujulint/check_spaces.py b/jujulint/checks/spaces.py
50similarity index 100%
51rename from jujulint/check_spaces.py
52rename to jujulint/checks/spaces.py
53diff --git a/jujulint/lint.py b/jujulint/lint.py
54index b8fa535..f73b7ea 100755
55--- a/jujulint/lint.py
56+++ b/jujulint/lint.py
57@@ -33,11 +33,12 @@ import yaml
58 from attr import attrib, attrs
59 from dateutil import relativedelta
60
61+import jujulint.checks.hyper_converged as hyper_converged
62 import jujulint.util as utils
63-from jujulint.check_spaces import Relation, find_space_mismatches
64+from jujulint.checks.relations import RelationError, RelationsRulesBootStrap
65+from jujulint.checks.spaces import Relation, find_space_mismatches
66 from jujulint.logging import Logger
67 from jujulint.model_input import input_handler
68-from jujulint.relations import RelationError, RelationsRulesBootStrap
69
70 VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte", "search")
71 VALID_LOG_LEVEL = {
72@@ -757,6 +758,33 @@ class Linter:
73 }
74 )
75
76+ def check_hyper_converged(self, input_file):
77+ """Check hyper converged deployments.
78+
79+ :param input_file: mapped content of the input file.
80+ :type input_file: Union[JujuBundleFile, JujuStatusFile]
81+ """
82+ hyper_converged_warning = hyper_converged.check_hyper_converged(input_file)
83+
84+ if hyper_converged_warning:
85+ for machine in hyper_converged_warning:
86+ for lxd in hyper_converged_warning[machine]:
87+ self.message_handler(
88+ {
89+ "id": "hyper-converged-masakari",
90+ "tags": ["hyper-converged", "masakari"],
91+ "message": (
92+ "Deployment has Masakari and the machine: '{}' "
93+ "has nova/osd and the lxd: '{}' with those services {}"
94+ ).format(
95+ machine,
96+ lxd,
97+ sorted(list(hyper_converged_warning[machine][lxd])),
98+ ),
99+ },
100+ log_level=logging.WARNING,
101+ )
102+
103 def check_charms_ops_mandatory(self, charm):
104 """
105 Check if a mandatory ops charms is present in the model.
106@@ -1352,6 +1380,7 @@ class Linter:
107
108 self.check_subs(parsed_yaml["machines"])
109 self.check_relations(input_file)
110+ self.check_hyper_converged(input_file)
111 self.check_charms()
112
113 if "relations" in parsed_yaml:
114diff --git a/jujulint/model_input.py b/jujulint/model_input.py
115index a14551c..8233ebb 100644
116--- a/jujulint/model_input.py
117+++ b/jujulint/model_input.py
118@@ -25,6 +25,7 @@ class BaseFile:
119 app_to_charm: Dict = field(default_factory=dict)
120 charm_to_app: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
121 apps_to_machines: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
122+ machines_to_apps: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
123
124 def __post_init__(self):
125 """Dunder method to map file after instantiating."""
126@@ -124,6 +125,20 @@ class BaseFile:
127 else set()
128 )
129
130+ def filter_machines_by_charm(self, charm: str) -> Set:
131+ """Filter machines that has a specific charm.
132+
133+ :param charm: Charm name.
134+ :type charm: str
135+ :return: Machines that contains the charm.
136+ :rtype: Set
137+ """
138+ charm_machines = set()
139+ charm_apps = self.charm_to_app[charm]
140+ for charm_app in charm_apps:
141+ charm_machines.update(self.apps_to_machines[charm_app])
142+ return charm_machines
143+
144 def map_machines(self):
145 """Map machines method to be implemented.
146
147@@ -158,6 +173,17 @@ class BaseFile:
148 """
149 raise NotImplementedError(f"{self.__class__.__name__} missing: sorted_machines")
150
151+ def filter_lxd_on_machine(self, machine: str):
152+ """Lxd containers on a machine.
153+
154+ :param machine: machine id.
155+ :type machine: str
156+ :raises NotImplementedError: Raise if not implemented on child classes.
157+ """
158+ raise NotImplementedError(
159+ f"{self.__class__.__name__} missing: filter_lxd_on_machine"
160+ )
161+
162
163 @dataclass
164 class JujuStatusFile(BaseFile):
165@@ -176,9 +202,12 @@ class JujuStatusFile(BaseFile):
166 for unit in units:
167 machine = units[unit].get("machine")
168 self.apps_to_machines[app].add(machine)
169+ self.machines_to_apps[machine].add(app)
170 subordinates = units[unit].get("subordinates", {})
171 for sub in subordinates:
172- self.apps_to_machines[sub.split("/")[0]].add(machine)
173+ sub_name = sub.split("/")[0]
174+ self.apps_to_machines[sub_name].add(machine)
175+ self.machines_to_apps[machine].add(sub_name)
176
177 @staticmethod
178 def sorted_machines(machine: str) -> Tuple[int, str, int]:
179@@ -218,6 +247,20 @@ class JujuStatusFile(BaseFile):
180 apps_related.update(relations.get(endpoint, []))
181 return apps_related
182
183+ def filter_lxd_on_machine(self, machine: str) -> Set:
184+ """Lxd containers on a machine.
185+
186+ :param machine: machine id.
187+ :type machine: str
188+ :return: lxd containers in the machine.
189+ :rtype: Set
190+ """
191+ return {
192+ lxd_machine
193+ for lxd_machine in self.machines
194+ if "lxd" in lxd_machine and lxd_machine.split("/")[0] == machine
195+ }
196+
197
198 @dataclass
199 class JujuBundleFile(BaseFile):
200@@ -241,18 +284,26 @@ class JujuBundleFile(BaseFile):
201 for app in self.applications_data:
202 machines = self.applications_data[app].get("to", [])
203 self.apps_to_machines[app].update(machines)
204+ for machine in machines:
205+ self.machines_to_apps[machine].add(app)
206 # NOTE(gabrielcocenza) subordinates won't have the 'to' field because
207 # they are deployed thru relations.
208 subordinates = {
209 sub for sub, machines in self.apps_to_machines.items() if machines == set()
210 }
211 for relation in self.relations_data:
212- app_1, endpoint_1, app_2, endpoint_2 = self.split_relation(relation)
213+ app_1, _, app_2, _ = self.split_relation(relation)
214 # update with the machines of the application that the subordinate charm relate.
215 if app_1 in subordinates:
216- self.apps_to_machines[app_1].update(self.apps_to_machines[app_2])
217+ sub_machines = self.apps_to_machines[app_2]
218+ self.apps_to_machines[app_1].update(sub_machines)
219+ for sub_machine in sub_machines:
220+ self.machines_to_apps[sub_machine].add(app_1)
221 elif app_2 in subordinates:
222+ sub_machines = self.apps_to_machines[app_1]
223 self.apps_to_machines[app_2].update(self.apps_to_machines[app_1])
224+ for sub_machine in sub_machines:
225+ self.machines_to_apps[sub_machine].add(app_2)
226
227 @staticmethod
228 def sorted_machines(machine: str) -> Tuple[int, str]:
229@@ -303,6 +354,20 @@ class JujuBundleFile(BaseFile):
230 apps_related.add(app_1_ep_1.split(":")[0])
231 return apps_related
232
233+ def filter_lxd_on_machine(self, machine: str) -> Set:
234+ """Lxd containers on a machine.
235+
236+ :param machine: machine id.
237+ :type machine: str
238+ :return: lxd containers in the machine.
239+ :rtype: Set
240+ """
241+ return {
242+ lxd_machine
243+ for lxd_machine in self.machines
244+ if "lxd" in lxd_machine and lxd_machine.split(":")[1] == machine
245+ }
246+
247
248 def input_handler(
249 parsed_yaml: Dict[str, Any], applications_key: str
250diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
251index b49a4b2..0f09bba 100644
252--- a/tests/unit/conftest.py
253+++ b/tests/unit/conftest.py
254@@ -242,17 +242,31 @@ def rules_files():
255
256
257 @pytest.fixture
258-def input_files(parsed_yaml_status, parsed_yaml_bundle):
259+def input_files(
260+ parsed_yaml_status,
261+ parsed_yaml_bundle,
262+ parsed_hyper_converged_yaml_status,
263+ parsed_hyper_converged_yaml_bundle,
264+):
265 return {
266 "juju-status": JujuStatusFile(
267 applications_data=parsed_yaml_status["applications"],
268 machines_data=parsed_yaml_status["machines"],
269 ),
270+ "juju-status-hyper-converged": JujuStatusFile(
271+ applications_data=parsed_hyper_converged_yaml_status["applications"],
272+ machines_data=parsed_hyper_converged_yaml_status["machines"],
273+ ),
274 "juju-bundle": JujuBundleFile(
275 applications_data=parsed_yaml_bundle["applications"],
276 machines_data=parsed_yaml_bundle["machines"],
277 relations_data=parsed_yaml_bundle["relations"],
278 ),
279+ "juju-bundle-parsed-hyper-converged": JujuBundleFile(
280+ applications_data=parsed_hyper_converged_yaml_bundle["applications"],
281+ machines_data=parsed_hyper_converged_yaml_bundle["machines"],
282+ relations_data=parsed_hyper_converged_yaml_bundle["relations"],
283+ ),
284 }
285
286
287@@ -476,3 +490,325 @@ def parsed_yaml_bundle():
288 ],
289 ],
290 }
291+
292+
293+@pytest.fixture
294+def parsed_hyper_converged_yaml_status():
295+ """Representation of a hyper converged model with masakari."""
296+ return {
297+ "machines": {
298+ "0": {
299+ "juju-status": {"current": "started"},
300+ "machine-status": {"current": "running"},
301+ "modification-status": {
302+ "current": "idle",
303+ },
304+ "containers": {
305+ "0/lxd/0": {
306+ "juju-status": {"current": "started"},
307+ "machine-status": {"current": "running"},
308+ "modification-status": {"current": "applied"},
309+ "constraints": "arch=amd64 spaces=",
310+ "hardware": "availability-zone=nova",
311+ },
312+ "0/lxd/1": {
313+ "juju-status": {"current": "started"},
314+ "machine-status": {"current": "running"},
315+ "modification-status": {"current": "applied"},
316+ "constraints": "arch=amd64 spaces=",
317+ "hardware": "availability-zone=nova",
318+ },
319+ },
320+ "constraints": "arch=amd64 mem=4096M",
321+ "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
322+ },
323+ "1": {
324+ "juju-status": {"current": "started"},
325+ "machine-status": {"current": "running"},
326+ "modification-status": {"current": "idle"},
327+ "constraints": "arch=amd64 mem=4096M",
328+ "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
329+ },
330+ "2": {
331+ "juju-status": {"current": "started"},
332+ "machine-status": {"current": "running"},
333+ "modification-status": {"current": "idle"},
334+ "constraints": "arch=amd64 mem=4096M",
335+ "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
336+ },
337+ "3": {
338+ "juju-status": {"current": "started"},
339+ "machine-status": {"current": "running"},
340+ "modification-status": {"current": "idle"},
341+ "constraints": "arch=amd64",
342+ "hardware": "arch=amd64 cores=1 mem=2048M root-disk=20480M availability-zone=nova",
343+ },
344+ },
345+ "applications": {
346+ "ceilometer": {
347+ "charm": "ceilometer",
348+ "charm-name": "ceilometer",
349+ "application-status": {"current": "active"},
350+ "relations": {"cluster": ["ceilometer"]},
351+ "units": {
352+ "ceilometer/0": {
353+ "workload-status": {"current": "active"},
354+ "juju-status": {"current": "idle"},
355+ "machine": "0/lxd/0",
356+ }
357+ },
358+ "endpoint-bindings": {
359+ "": "alpha",
360+ "admin": "alpha",
361+ "amqp": "alpha",
362+ "amqp-listener": "alpha",
363+ "ceilometer-service": "alpha",
364+ "certificates": "alpha",
365+ "cluster": "alpha",
366+ "event-service": "alpha",
367+ "ha": "alpha",
368+ "identity-credentials": "alpha",
369+ "identity-notifications": "alpha",
370+ "identity-service": "alpha",
371+ "internal": "alpha",
372+ "metric-service": "alpha",
373+ "nrpe-external-master": "alpha",
374+ "public": "alpha",
375+ "shared-db": "alpha",
376+ },
377+ },
378+ "ceph-mon": {
379+ "charm": "ceph-mon",
380+ "charm-name": "ceph-mon",
381+ "application-status": {"current": "active"},
382+ "relations": {
383+ "client": ["nova-compute"],
384+ "mon": ["ceph-mon"],
385+ "osd": ["ceph-osd"],
386+ },
387+ "units": {
388+ "ceph-mon/0": {
389+ "workload-status": {"current": "idle"},
390+ "juju-status": {"current": "idle"},
391+ "machine": "0",
392+ },
393+ "ceph-mon/1": {
394+ "workload-status": {"current": "idle"},
395+ "juju-status": {"current": "idle"},
396+ "machine": "1",
397+ },
398+ "ceph-mon/2": {
399+ "workload-status": {"current": "idle"},
400+ "juju-status": {"current": "idle"},
401+ "machine": "2",
402+ },
403+ },
404+ "endpoint-bindings": {
405+ "": "alpha",
406+ "admin": "alpha",
407+ "bootstrap-source": "alpha",
408+ "client": "alpha",
409+ "cluster": "alpha",
410+ "dashboard": "alpha",
411+ "mds": "alpha",
412+ "mon": "alpha",
413+ "nrpe-external-master": "alpha",
414+ "osd": "alpha",
415+ "prometheus": "alpha",
416+ "public": "alpha",
417+ "radosgw": "alpha",
418+ "rbd-mirror": "alpha",
419+ },
420+ },
421+ "ceph-osd": {
422+ "charm": "ceph-osd",
423+ "charm-name": "ceph-osd",
424+ "application-status": {"current": "active"},
425+ "relations": {"mon": ["ceph-mon"]},
426+ "units": {
427+ "ceph-osd/0": {
428+ "workload-status": {"current": "idle"},
429+ "juju-status": {"current": "idle"},
430+ "machine": "0",
431+ },
432+ "ceph-osd/1": {
433+ "workload-status": {"current": "idle"},
434+ "juju-status": {"current": "idle"},
435+ "machine": "1",
436+ },
437+ "ceph-osd/2": {
438+ "workload-status": {"current": "idle"},
439+ "juju-status": {"current": "idle"},
440+ "machine": "2",
441+ },
442+ },
443+ "endpoint-bindings": {
444+ "": "alpha",
445+ "cluster": "alpha",
446+ "mon": "alpha",
447+ "nrpe-external-master": "alpha",
448+ "public": "alpha",
449+ "secrets-storage": "alpha",
450+ },
451+ },
452+ "heat": {
453+ "charm": "heat",
454+ "series": "focal",
455+ "charm-name": "heat",
456+ "application-status": {"current": "active"},
457+ "relations": {"cluster": ["heat"]},
458+ "units": {
459+ "heat/0": {
460+ "workload-status": {"current": "idle"},
461+ "juju-status": {"current": "idle"},
462+ "machine": "0/lxd/1",
463+ }
464+ },
465+ "endpoint-bindings": {
466+ "": "alpha",
467+ "admin": "alpha",
468+ "amqp": "alpha",
469+ "certificates": "alpha",
470+ "cluster": "alpha",
471+ "ha": "alpha",
472+ "heat-plugin-subordinate": "alpha",
473+ "identity-service": "alpha",
474+ "internal": "alpha",
475+ "nrpe-external-master": "alpha",
476+ "public": "alpha",
477+ "shared-db": "alpha",
478+ },
479+ },
480+ "masakari": {
481+ "charm": "masakari",
482+ "charm-name": "masakari",
483+ "application-status": {"current": "active"},
484+ "relations": {"cluster": ["masakari"]},
485+ "units": {
486+ "masakari/0": {
487+ "workload-status": {"current": "idle"},
488+ "juju-status": {"current": "idle"},
489+ "machine": "3",
490+ }
491+ },
492+ "endpoint-bindings": {
493+ "": "alpha",
494+ "admin": "alpha",
495+ "amqp": "alpha",
496+ "certificates": "alpha",
497+ "cluster": "alpha",
498+ "ha": "alpha",
499+ "identity-service": "alpha",
500+ "internal": "alpha",
501+ "public": "alpha",
502+ "shared-db": "alpha",
503+ },
504+ },
505+ "nova-compute": {
506+ "charm": "nova-compute",
507+ "charm-name": "nova-compute",
508+ "application-status": {"current": "active"},
509+ "relations": {"ceph": ["ceph-mon"], "compute-peer": ["nova-compute"]},
510+ "units": {
511+ "nova-compute/0": {
512+ "workload-status": {"current": "idle"},
513+ "juju-status": {"current": "idle"},
514+ "machine": "0",
515+ },
516+ "nova-compute/1": {
517+ "workload-status": {"current": "idle"},
518+ "juju-status": {"current": "idle"},
519+ "machine": "1",
520+ },
521+ "nova-compute/2": {
522+ "workload-status": {"current": "idle"},
523+ "juju-status": {"current": "idle"},
524+ "machine": "2",
525+ },
526+ },
527+ "endpoint-bindings": {
528+ "": "alpha",
529+ "amqp": "alpha",
530+ "ceph": "alpha",
531+ "ceph-access": "alpha",
532+ "cloud-compute": "alpha",
533+ "cloud-credentials": "alpha",
534+ "compute-peer": "alpha",
535+ "ephemeral-backend": "alpha",
536+ "image-service": "alpha",
537+ "internal": "alpha",
538+ "ironic-api": "alpha",
539+ "lxd": "alpha",
540+ "migration": "alpha",
541+ "neutron-plugin": "alpha",
542+ "nova-ceilometer": "alpha",
543+ "nrpe-external-master": "alpha",
544+ "secrets-storage": "alpha",
545+ },
546+ },
547+ },
548+ }
549+
550+
551+@pytest.fixture
552+def parsed_hyper_converged_yaml_bundle():
553+ """Representation of a hyper converged model with masakari."""
554+ return {
555+ "series": "focal",
556+ "applications": {
557+ "ceilometer": {
558+ "charm": "ceilometer",
559+ "num_units": 1,
560+ "to": ["lxd:0"],
561+ "constraints": "arch=amd64",
562+ },
563+ "ceph-mon": {
564+ "charm": "ceph-mon",
565+ "num_units": 3,
566+ "to": ["0", "1", "2"],
567+ "constraints": "arch=amd64",
568+ },
569+ "ceph-osd": {
570+ "charm": "ceph-osd",
571+ "num_units": 3,
572+ "to": ["0", "1", "2"],
573+ "constraints": "arch=amd64 mem=4096",
574+ "storage": {
575+ "bluestore-db": "loop,0,1024",
576+ "bluestore-wal": "loop,0,1024",
577+ "osd-devices": "loop,0,1024",
578+ "osd-journals": "loop,0,1024",
579+ },
580+ },
581+ "heat": {
582+ "charm": "heat",
583+ "resources": {"policyd-override": 0},
584+ "num_units": 1,
585+ "to": ["lxd:0"],
586+ "constraints": "arch=amd64",
587+ },
588+ "masakari": {
589+ "charm": "masakari",
590+ "num_units": 1,
591+ "to": ["3"],
592+ "constraints": "arch=amd64",
593+ },
594+ "nova-compute": {
595+ "charm": "nova-compute",
596+ "num_units": 3,
597+ "to": ["0", "1", "2"],
598+ "constraints": "arch=amd64",
599+ "storage": {"ephemeral-device": "loop,0,10240"},
600+ },
601+ },
602+ "machines": {
603+ "0": {"constraints": "arch=amd64 mem=4096"},
604+ "1": {"constraints": "arch=amd64 mem=4096"},
605+ "2": {"constraints": "arch=amd64 mem=4096"},
606+ "3": {"constraints": "arch=amd64"},
607+ },
608+ "relations": [
609+ ["ceph-mon:client", "nova-compute:ceph"],
610+ ["ceph-mon:osd", "ceph-osd:mon"],
611+ ],
612+ }
613diff --git a/tests/unit/test_hyper_converged.py b/tests/unit/test_hyper_converged.py
614new file mode 100644
615index 0000000..751af7a
616--- /dev/null
617+++ b/tests/unit/test_hyper_converged.py
618@@ -0,0 +1,32 @@
619+from collections import defaultdict
620+
621+import pytest
622+
623+from jujulint.checks import hyper_converged
624+
625+
626+@pytest.mark.parametrize(
627+ "masakari, input_file_type",
628+ [
629+ (True, "juju-status-hyper-converged"),
630+ (False, "juju-status-hyper-converged"),
631+ (True, "juju-bundle-parsed-hyper-converged"),
632+ (False, "juju-bundle-parsed-hyper-converged"),
633+ ],
634+)
635+def test_check_hyper_converged(input_files, masakari, input_file_type):
636+ """Test hyper_converged models."""
637+ input_file = input_files[input_file_type]
638+ expected_result = defaultdict(lambda: defaultdict(set))
639+ if masakari and "juju-status" in input_file_type:
640+ expected_result["0"]["0/lxd/0"] = {"ceilometer"}
641+ expected_result["0"]["0/lxd/1"] = {"heat"}
642+ elif masakari and "juju-bundle" in input_file_type:
643+ expected_result["0"]["lxd:0"] = {"ceilometer", "heat"}
644+ else:
645+ # remove masakari from input file
646+ del input_file.applications_data["masakari"]
647+ del input_file.machines_data["3"]
648+ input_file.charms = set()
649+ input_file.map_file()
650+ assert hyper_converged.check_hyper_converged(input_file) == expected_result
651diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py
652index 682b664..f06ff4b 100644
653--- a/tests/unit/test_input.py
654+++ b/tests/unit/test_input.py
655@@ -51,6 +51,18 @@ def test_file_inputs(input_files, input_file_type):
656 "keystone": {"lxd:1"},
657 },
658 },
659+ "machines_to_apps": {
660+ "juju-status": {
661+ "0": {"nrpe-host", "elasticsearch"},
662+ "1": {"ubuntu", "nrpe-host"},
663+ "1/lxd/0": {"nrpe-container", "keystone"},
664+ },
665+ "juju-bundle": {
666+ "0": {"nrpe-host", "elasticsearch"},
667+ "1": {"ubuntu", "nrpe-host"},
668+ "lxd:1": {"nrpe-container", "keystone"},
669+ },
670+ },
671 }
672 assert input_file.applications == expected_output["applications"]
673 assert input_file.machines == expected_output["machines"][input_file_type]
674@@ -58,6 +70,10 @@ def test_file_inputs(input_files, input_file_type):
675 input_file.apps_to_machines
676 == expected_output["apps_to_machines"][input_file_type]
677 )
678+ assert (
679+ input_file.machines_to_apps
680+ == expected_output["machines_to_apps"][input_file_type]
681+ )
682 assert input_file.charms == expected_output["charms"]
683 assert input_file.app_to_charm == expected_output["app_to_charm"]
684 assert input_file.charm_to_app == expected_output["charm_to_app"]
685@@ -207,6 +223,48 @@ def test_input_handler(parsed_yaml, expected_output, request):
686 )
687
688
689+@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
690+def test_filter_machines_by_charm(input_files, input_file_type):
691+ """Test filter_machines_by_charm method."""
692+ input_file = input_files[input_file_type]
693+ if input_file_type == "juju-status":
694+ expected_output = {
695+ "nrpe": {"0", "1", "1/lxd/0"},
696+ "keystone": {"1/lxd/0"},
697+ "ubuntu": {"1"},
698+ "elasticsearch": {"0"},
699+ }
700+ else:
701+ expected_output = {
702+ "nrpe": {"0", "1", "lxd:1"},
703+ "keystone": {"lxd:1"},
704+ "ubuntu": {"1"},
705+ "elasticsearch": {"0"},
706+ }
707+ for charm in input_file.charms:
708+ assert input_file.filter_machines_by_charm(charm) == expected_output[charm]
709+
710+
711+@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
712+def test_filter_lxd_on_machine(input_files, input_file_type):
713+ """Test filter_lxd_on_machine method."""
714+ input_file = input_files[input_file_type]
715+ if input_file_type == "juju-status":
716+ expected_output = {
717+ "0": set(),
718+ "1": {"1/lxd/0"},
719+ "1/lxd/0": set(),
720+ }
721+ else:
722+ expected_output = {
723+ "0": set(),
724+ "1": {"lxd:1"},
725+ "lxd:1": set(),
726+ }
727+ for machine in input_file.machines:
728+ assert input_file.filter_lxd_on_machine(machine) == expected_output[machine]
729+
730+
731 def test_raise_not_implemented_methods(parsed_yaml_status):
732 # declare a new input class
733 @dataclass
734@@ -231,3 +289,6 @@ def test_raise_not_implemented_methods(parsed_yaml_status):
735
736 with pytest.raises(NotImplementedError):
737 new_input.sorted_machines("0")
738+
739+ with pytest.raises(NotImplementedError):
740+ new_input.filter_lxd_on_machine("0")
741diff --git a/tests/unit/test_jujulint.py b/tests/unit/test_jujulint.py
742index 77dbb5e..c87722d 100644
743--- a/tests/unit/test_jujulint.py
744+++ b/tests/unit/test_jujulint.py
745@@ -7,7 +7,8 @@ from unittest import mock
746 import pytest
747 import yaml
748
749-from jujulint import check_spaces, lint, relations
750+from jujulint import lint
751+from jujulint.checks import relations, spaces
752 from jujulint.lint import VALID_LOG_LEVEL
753
754
755@@ -1424,7 +1425,7 @@ applications:
756 This warning should be triggerred if some applications have bindings and some
757 dont.
758 """
759- logger_mock = mocker.patch.object(check_spaces, "LOGGER")
760+ logger_mock = mocker.patch.object(spaces, "LOGGER")
761
762 app_without_binding = "prometheus-app"
763 bundle = {
764@@ -1458,7 +1459,7 @@ applications:
765 mentioned explicitly will be bound to this default space.
766 Juju lint should raise warning if bundles do not define default space.
767 """
768- logger_mock = mocker.patch.object(check_spaces, "LOGGER")
769+ logger_mock = mocker.patch.object(spaces, "LOGGER")
770 app_without_default_space = "telegraf-app"
771
772 bundle = {
773@@ -1490,7 +1491,7 @@ applications:
774
775 def test_check_spaces_multi_model_warning(self, linter, mocker):
776 """Test that check_spaces shows warning if some application are from another model."""
777- logger_mock = mocker.patch.object(check_spaces, "LOGGER")
778+ logger_mock = mocker.patch.object(spaces, "LOGGER")
779
780 app_another_model = "prometheus-app"
781 bundle = {
782@@ -1745,3 +1746,60 @@ applications:
783 logger_mock.assert_has_calls(
784 [mocker.call(expected_message, level=logging.ERROR)]
785 )
786+
787+ @pytest.mark.parametrize(
788+ "input_file_type",
789+ ["juju-status-hyper-converged", "juju-bundle-parsed-hyper-converged"],
790+ )
791+ def test_check_hyper_converged(self, linter, input_files, mocker, input_file_type):
792+ """Test check_hyper_converged."""
793+ input_file = input_files[input_file_type]
794+ mock_message_handler = mocker.patch("jujulint.lint.Linter.message_handler")
795+ msg = (
796+ "Deployment has Masakari and the machine: '{}' "
797+ "has nova/osd and the lxd: '{}' with those services {}"
798+ )
799+ expected_output = [
800+ mocker.call(
801+ {
802+ "id": "hyper-converged-masakari",
803+ "tags": ["hyper-converged", "masakari"],
804+ "message": msg.format(
805+ "0",
806+ "lxd:0",
807+ ["ceilometer", "heat"],
808+ ),
809+ },
810+ log_level=logging.WARNING,
811+ )
812+ ]
813+ if "juju-status" in input_file_type:
814+ expected_output = [
815+ mocker.call(
816+ {
817+ "id": "hyper-converged-masakari",
818+ "tags": ["hyper-converged", "masakari"],
819+ "message": msg.format(
820+ "0",
821+ "0/lxd/0",
822+ ["ceilometer"],
823+ ),
824+ },
825+ log_level=logging.WARNING,
826+ ),
827+ mocker.call(
828+ {
829+ "id": "hyper-converged-masakari",
830+ "tags": ["hyper-converged", "masakari"],
831+ "message": msg.format(
832+ "0",
833+ "0/lxd/1",
834+ ["heat"],
835+ ),
836+ },
837+ log_level=logging.WARNING,
838+ ),
839+ ]
840+
841+ linter.check_hyper_converged(input_file)
842+ mock_message_handler.assert_has_calls(expected_output, any_order=True)
843diff --git a/tests/unit/test_relations.py b/tests/unit/test_relations.py
844index f13d544..121fe9e 100644
845--- a/tests/unit/test_relations.py
846+++ b/tests/unit/test_relations.py
847@@ -2,7 +2,7 @@
848 """Test the relations module."""
849 import pytest
850
851-from jujulint import relations
852+from jujulint.checks import relations
853
854 CHARM_TO_APP = {"nrpe-host", "nrpe-container"}
855 CHARM = "nrpe"
856@@ -352,7 +352,7 @@ def test_relations_raise_not_implemented(input_files, mocker):
857 """Ensure that a new class that not implement mandatory methods raises error."""
858 logger_mock = mocker.patch.object(relations, "LOGGER")
859 mocker.patch(
860- "jujulint.relations.RelationRule.relation_exist_check",
861+ "jujulint.checks.relations.RelationRule.relation_exist_check",
862 side_effect=NotImplementedError(),
863 )
864 input_file = input_files["juju-status"]
865diff --git a/tests/unit/test_check_spaces.py b/tests/unit/test_spaces.py
866similarity index 80%
867rename from tests/unit/test_check_spaces.py
868rename to tests/unit/test_spaces.py
869index 9b77e66..db16505 100644
870--- a/tests/unit/test_check_spaces.py
871+++ b/tests/unit/test_spaces.py
872@@ -1,9 +1,9 @@
873-"""Tests for check_spaces.py module."""
874+"""Tests for spaces.py module."""
875 from unittest.mock import call
876
877 import pytest
878
879-from jujulint import check_spaces
880+from jujulint.checks import spaces
881
882
883 def test_relation_init():
884@@ -11,7 +11,7 @@ def test_relation_init():
885 ep_1 = "Endpoint 1"
886 ep_2 = "Endpoint 2"
887
888- relation = check_spaces.Relation(ep_1, ep_2)
889+ relation = spaces.Relation(ep_1, ep_2)
890
891 assert relation.endpoint1 == ep_1
892 assert relation.endpoint2 == ep_2
893@@ -23,7 +23,7 @@ def test_relation_str():
894 ep_2 = "Endpoint 2"
895 expected_str = "Relation({} - {})".format(ep_1, ep_2)
896
897- relation = check_spaces.Relation(ep_1, ep_2)
898+ relation = spaces.Relation(ep_1, ep_2)
899
900 assert str(relation) == expected_str
901
902@@ -63,8 +63,8 @@ def test_relation_str():
903 )
904 def test_relation_eq(rel_1_ep_1, rel_1_ep_2, rel_2_ep_1, rel_2_ep_2, expected_result):
905 """Test equality operator of Relation class. Only return true if both endpoints match."""
906- relation_1 = check_spaces.Relation(rel_1_ep_1, rel_1_ep_2)
907- relation_2 = check_spaces.Relation(rel_2_ep_1, rel_2_ep_2)
908+ relation_1 = spaces.Relation(rel_1_ep_1, rel_1_ep_2)
909+ relation_2 = spaces.Relation(rel_2_ep_1, rel_2_ep_2)
910
911 assert (relation_1 == relation_2) == expected_result
912
913@@ -74,7 +74,7 @@ def test_relation_endpoints_prop():
914 ep_1 = "Endpoint 1"
915 ep_2 = "Endpoint 2"
916
917- relation = check_spaces.Relation(ep_1, ep_2)
918+ relation = spaces.Relation(ep_1, ep_2)
919
920 assert relation.endpoints == [ep_1, ep_2]
921
922@@ -105,7 +105,7 @@ def test_space_mismatch_init(input_order, output_order):
923 This test also verifies that spaces in SpaceMismatch instance are ordered
924 alphabetically based on the endpoint name.
925 """
926- mismatch_instance = check_spaces.SpaceMismatch(*input_order)
927+ mismatch_instance = spaces.SpaceMismatch(*input_order)
928
929 # Assert that endpoints are alphabetically reordered
930 assert mismatch_instance.endpoint1 == output_order[0]
931@@ -124,7 +124,7 @@ def test_space_mismatch_str():
932 ep_1, space_1, ep_2, space_2
933 )
934
935- mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
936+ mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
937
938 assert str(mismatch_instance) == expected_str
939
940@@ -136,9 +136,9 @@ def test_space_mismatch_relation_prop():
941 space_1 = "Space 1"
942 space_2 = "Space 2"
943
944- expected_relation = check_spaces.Relation(ep_1, ep_2)
945+ expected_relation = spaces.Relation(ep_1, ep_2)
946
947- mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
948+ mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
949
950 assert mismatch_instance.relation == expected_relation
951
952@@ -156,9 +156,9 @@ def test_space_mismatch_get_charm_relation():
953
954 app_map = {app_1: charm_1, app_2: charm_2}
955
956- expected_relation = check_spaces.Relation("ubuntu:endpoint_1", "nrpe:endpoint_2")
957+ expected_relation = spaces.Relation("ubuntu:endpoint_1", "nrpe:endpoint_2")
958
959- mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
960+ mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
961
962 assert mismatch_instance.get_charm_relation(app_map) == expected_relation
963
964@@ -173,32 +173,28 @@ def test_find_space_mismatches(use_cmr, mocker):
965 space_2 = "space 2"
966 app_endpoint_1 = app_1 + ":endpoint"
967 app_endpoint_2 = app_2 + ":endpoint"
968- relation = check_spaces.Relation(
969- app_endpoint_1, "XModel" if use_cmr else app_endpoint_2
970- )
971+ relation = spaces.Relation(app_endpoint_1, "XModel" if use_cmr else app_endpoint_2)
972 app_list = [app_1, app_2]
973 app_spaces = {app_1: {space_1: "foo"}, app_2: {space_2: "bar"}}
974
975 app_list_mock = mocker.patch.object(
976- check_spaces, "get_juju_applications", return_value=app_list
977+ spaces, "get_juju_applications", return_value=app_list
978 )
979 app_spaces_mock = mocker.patch.object(
980- check_spaces, "get_application_spaces", return_value=app_spaces
981+ spaces, "get_application_spaces", return_value=app_spaces
982 )
983 rel_list_mock = mocker.patch.object(
984- check_spaces, "get_application_relations", return_value=[relation]
985+ spaces, "get_application_relations", return_value=[relation]
986 )
987 rel_space_mock = mocker.patch.object(
988- check_spaces, "get_relation_space", side_effect=[space_1, space_2]
989+ spaces, "get_relation_space", side_effect=[space_1, space_2]
990 )
991
992 expected_mismatch = [
993- check_spaces.SpaceMismatch(
994- relation.endpoint1, space_1, relation.endpoint2, space_2
995- )
996+ spaces.SpaceMismatch(relation.endpoint1, space_1, relation.endpoint2, space_2)
997 ]
998
999- mismatch = check_spaces.find_space_mismatches(sample_yaml, True)
1000+ mismatch = spaces.find_space_mismatches(sample_yaml, True)
1001 result_pairs = zip(expected_mismatch, mismatch)
1002
1003 app_list_mock.assert_called_once_with(sample_yaml)
1004@@ -224,7 +220,7 @@ def test_get_juju_applications():
1005
1006 expected_apps = [app_1, app_2]
1007
1008- apps = check_spaces.get_juju_applications(sample_yaml)
1009+ apps = spaces.get_juju_applications(sample_yaml)
1010
1011 assert apps == expected_apps
1012
1013@@ -235,7 +231,7 @@ def test_get_application_spaces(mocker):
1014 This test also verifies that default binding to space "alpha" is added to applications
1015 that do not specify any bindings.
1016 """
1017- logger_mock = mocker.patch.object(check_spaces, "LOGGER")
1018+ logger_mock = mocker.patch.object(spaces, "LOGGER")
1019 default_binding = ""
1020 default_space = "custom_default_space"
1021 public_binding = "public"
1022@@ -269,7 +265,7 @@ def test_get_application_spaces(mocker):
1023 app_list[2]: {default_binding: "alpha"},
1024 }
1025
1026- app_spaces = check_spaces.get_application_spaces(app_list, sample_yaml)
1027+ app_spaces = spaces.get_application_spaces(app_list, sample_yaml)
1028
1029 # Verify that all the bindings for properly defined app were returned
1030 # Verify that default binding was added to app that did not have any bindings defined
1031@@ -298,11 +294,11 @@ def test_get_application_relations():
1032 }
1033
1034 expected_relations = [
1035- check_spaces.Relation("ubuntu:juju-info", "nrpe:general-info"),
1036- check_spaces.Relation("vault:shared-db", "mysql-innodb-cluster:shared-db"),
1037+ spaces.Relation("ubuntu:juju-info", "nrpe:general-info"),
1038+ spaces.Relation("vault:shared-db", "mysql-innodb-cluster:shared-db"),
1039 ]
1040
1041- relations = check_spaces.get_application_relations(sample_yaml)
1042+ relations = spaces.get_application_relations(sample_yaml)
1043
1044 assert relations == expected_relations
1045
1046@@ -323,21 +319,21 @@ def test_get_relation_space(use_explicit_binding):
1047 else:
1048 expected_space = default_space
1049
1050- space = check_spaces.get_relation_space(endpoint, app_spaces)
1051+ space = spaces.get_relation_space(endpoint, app_spaces)
1052
1053 assert space == expected_space
1054
1055
1056 def test_get_relation_space_cmr(mocker):
1057 """Test getting space for cross model relation."""
1058- logger_mock = mocker.patch.object(check_spaces, "LOGGER")
1059+ logger_mock = mocker.patch.object(spaces, "LOGGER")
1060 app_name = "ubuntu"
1061 interface = "juju_info"
1062 endpoint = app_name + ":" + interface
1063
1064 app_spaces = {}
1065
1066- space = check_spaces.get_relation_space(endpoint, app_spaces)
1067+ space = spaces.get_relation_space(endpoint, app_spaces)
1068
1069 assert space == "XModel"
1070 logger_mock.warning.assert_called_once_with(
1071diff --git a/tox.ini b/tox.ini
1072index 4dea114..08183e2 100644
1073--- a/tox.ini
1074+++ b/tox.ini
1075@@ -23,7 +23,7 @@ deps =
1076 -r{toxinidir}/requirements.txt
1077 -r{toxinidir}/tests/unit/requirements.txt
1078 commands =
1079- pytest -v \
1080+ pytest -vv \
1081 --cov=jujulint \
1082 --new-first \
1083 --last-failed \

Subscribers

People subscribed via source and target branches