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 (community) 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
diff --git a/jujulint/checks/hyper_converged.py b/jujulint/checks/hyper_converged.py
0new file mode 1006440new file mode 100644
index 0000000..0dd5da6
--- /dev/null
+++ b/jujulint/checks/hyper_converged.py
@@ -0,0 +1,38 @@
1#!/usr/bin/python3
2"""Checks if nodes can be Hyper-Converged."""
3
4from collections import defaultdict
5from typing import DefaultDict, Union
6
7from jujulint.model_input import JujuBundleFile, JujuStatusFile
8
9
10# see LP#1990885
11def check_hyper_converged(
12 input_file: Union[JujuBundleFile, JujuStatusFile]
13) -> DefaultDict[str, DefaultDict[str, set]]:
14 """Check if other services are collocated with nova/osd with masakari.
15
16 Hyperconvered is nova/osd collocated with openstack services.
17 Masakari uses ha-cluster to monitor nodes. If the node is not responsive then the
18 node is taken down. This is fine for nova/osd units, but if there are collocated
19 with openstack services this can be problematic.
20
21
22 :param input_file: mapped content of the input file.
23 :type input_file: Union[JujuBundleFile, JujuStatusFile]
24 :return: Services on lxds that are on nova/osd machines.
25 :rtype: DefaultDict[str, DefaultDict[str, set]]
26 """
27 hyper_converged_warning = defaultdict(lambda: defaultdict(set))
28 if "masakari" in input_file.charms:
29 nova_machines = input_file.filter_machines_by_charm("nova-compute")
30 ods_machines = input_file.filter_machines_by_charm("ceph-osd")
31 nova_osd_machines = nova_machines.intersection(ods_machines)
32 if nova_osd_machines:
33 for machine in nova_osd_machines:
34 lxds = input_file.filter_lxd_on_machine(machine)
35 for lxd in lxds:
36 apps = input_file.machines_to_apps[lxd]
37 hyper_converged_warning[machine][lxd] = apps
38 return hyper_converged_warning
diff --git a/jujulint/relations.py b/jujulint/checks/relations.py
0similarity index 100%39similarity index 100%
1rename from jujulint/relations.py40rename from jujulint/relations.py
2rename to jujulint/checks/relations.py41rename to jujulint/checks/relations.py
diff --git a/jujulint/check_spaces.py b/jujulint/checks/spaces.py
3similarity index 100%42similarity index 100%
4rename from jujulint/check_spaces.py43rename from jujulint/check_spaces.py
5rename to jujulint/checks/spaces.py44rename to jujulint/checks/spaces.py
diff --git a/jujulint/lint.py b/jujulint/lint.py
index b8fa535..f73b7ea 100755
--- a/jujulint/lint.py
+++ b/jujulint/lint.py
@@ -33,11 +33,12 @@ import yaml
33from attr import attrib, attrs33from attr import attrib, attrs
34from dateutil import relativedelta34from dateutil import relativedelta
3535
36import jujulint.checks.hyper_converged as hyper_converged
36import jujulint.util as utils37import jujulint.util as utils
37from jujulint.check_spaces import Relation, find_space_mismatches38from jujulint.checks.relations import RelationError, RelationsRulesBootStrap
39from jujulint.checks.spaces import Relation, find_space_mismatches
38from jujulint.logging import Logger40from jujulint.logging import Logger
39from jujulint.model_input import input_handler41from jujulint.model_input import input_handler
40from jujulint.relations import RelationError, RelationsRulesBootStrap
4142
42VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte", "search")43VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte", "search")
43VALID_LOG_LEVEL = {44VALID_LOG_LEVEL = {
@@ -757,6 +758,33 @@ class Linter:
757 }758 }
758 )759 )
759760
761 def check_hyper_converged(self, input_file):
762 """Check hyper converged deployments.
763
764 :param input_file: mapped content of the input file.
765 :type input_file: Union[JujuBundleFile, JujuStatusFile]
766 """
767 hyper_converged_warning = hyper_converged.check_hyper_converged(input_file)
768
769 if hyper_converged_warning:
770 for machine in hyper_converged_warning:
771 for lxd in hyper_converged_warning[machine]:
772 self.message_handler(
773 {
774 "id": "hyper-converged-masakari",
775 "tags": ["hyper-converged", "masakari"],
776 "message": (
777 "Deployment has Masakari and the machine: '{}' "
778 "has nova/osd and the lxd: '{}' with those services {}"
779 ).format(
780 machine,
781 lxd,
782 sorted(list(hyper_converged_warning[machine][lxd])),
783 ),
784 },
785 log_level=logging.WARNING,
786 )
787
760 def check_charms_ops_mandatory(self, charm):788 def check_charms_ops_mandatory(self, charm):
761 """789 """
762 Check if a mandatory ops charms is present in the model.790 Check if a mandatory ops charms is present in the model.
@@ -1352,6 +1380,7 @@ class Linter:
13521380
1353 self.check_subs(parsed_yaml["machines"])1381 self.check_subs(parsed_yaml["machines"])
1354 self.check_relations(input_file)1382 self.check_relations(input_file)
1383 self.check_hyper_converged(input_file)
1355 self.check_charms()1384 self.check_charms()
13561385
1357 if "relations" in parsed_yaml:1386 if "relations" in parsed_yaml:
diff --git a/jujulint/model_input.py b/jujulint/model_input.py
index a14551c..8233ebb 100644
--- a/jujulint/model_input.py
+++ b/jujulint/model_input.py
@@ -25,6 +25,7 @@ class BaseFile:
25 app_to_charm: Dict = field(default_factory=dict)25 app_to_charm: Dict = field(default_factory=dict)
26 charm_to_app: defaultdict[set] = field(default_factory=lambda: defaultdict(set))26 charm_to_app: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
27 apps_to_machines: defaultdict[set] = field(default_factory=lambda: defaultdict(set))27 apps_to_machines: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
28 machines_to_apps: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
2829
29 def __post_init__(self):30 def __post_init__(self):
30 """Dunder method to map file after instantiating."""31 """Dunder method to map file after instantiating."""
@@ -124,6 +125,20 @@ class BaseFile:
124 else set()125 else set()
125 )126 )
126127
128 def filter_machines_by_charm(self, charm: str) -> Set:
129 """Filter machines that has a specific charm.
130
131 :param charm: Charm name.
132 :type charm: str
133 :return: Machines that contains the charm.
134 :rtype: Set
135 """
136 charm_machines = set()
137 charm_apps = self.charm_to_app[charm]
138 for charm_app in charm_apps:
139 charm_machines.update(self.apps_to_machines[charm_app])
140 return charm_machines
141
127 def map_machines(self):142 def map_machines(self):
128 """Map machines method to be implemented.143 """Map machines method to be implemented.
129144
@@ -158,6 +173,17 @@ class BaseFile:
158 """173 """
159 raise NotImplementedError(f"{self.__class__.__name__} missing: sorted_machines")174 raise NotImplementedError(f"{self.__class__.__name__} missing: sorted_machines")
160175
176 def filter_lxd_on_machine(self, machine: str):
177 """Lxd containers on a machine.
178
179 :param machine: machine id.
180 :type machine: str
181 :raises NotImplementedError: Raise if not implemented on child classes.
182 """
183 raise NotImplementedError(
184 f"{self.__class__.__name__} missing: filter_lxd_on_machine"
185 )
186
161187
162@dataclass188@dataclass
163class JujuStatusFile(BaseFile):189class JujuStatusFile(BaseFile):
@@ -176,9 +202,12 @@ class JujuStatusFile(BaseFile):
176 for unit in units:202 for unit in units:
177 machine = units[unit].get("machine")203 machine = units[unit].get("machine")
178 self.apps_to_machines[app].add(machine)204 self.apps_to_machines[app].add(machine)
205 self.machines_to_apps[machine].add(app)
179 subordinates = units[unit].get("subordinates", {})206 subordinates = units[unit].get("subordinates", {})
180 for sub in subordinates:207 for sub in subordinates:
181 self.apps_to_machines[sub.split("/")[0]].add(machine)208 sub_name = sub.split("/")[0]
209 self.apps_to_machines[sub_name].add(machine)
210 self.machines_to_apps[machine].add(sub_name)
182211
183 @staticmethod212 @staticmethod
184 def sorted_machines(machine: str) -> Tuple[int, str, int]:213 def sorted_machines(machine: str) -> Tuple[int, str, int]:
@@ -218,6 +247,20 @@ class JujuStatusFile(BaseFile):
218 apps_related.update(relations.get(endpoint, []))247 apps_related.update(relations.get(endpoint, []))
219 return apps_related248 return apps_related
220249
250 def filter_lxd_on_machine(self, machine: str) -> Set:
251 """Lxd containers on a machine.
252
253 :param machine: machine id.
254 :type machine: str
255 :return: lxd containers in the machine.
256 :rtype: Set
257 """
258 return {
259 lxd_machine
260 for lxd_machine in self.machines
261 if "lxd" in lxd_machine and lxd_machine.split("/")[0] == machine
262 }
263
221264
222@dataclass265@dataclass
223class JujuBundleFile(BaseFile):266class JujuBundleFile(BaseFile):
@@ -241,18 +284,26 @@ class JujuBundleFile(BaseFile):
241 for app in self.applications_data:284 for app in self.applications_data:
242 machines = self.applications_data[app].get("to", [])285 machines = self.applications_data[app].get("to", [])
243 self.apps_to_machines[app].update(machines)286 self.apps_to_machines[app].update(machines)
287 for machine in machines:
288 self.machines_to_apps[machine].add(app)
244 # NOTE(gabrielcocenza) subordinates won't have the 'to' field because289 # NOTE(gabrielcocenza) subordinates won't have the 'to' field because
245 # they are deployed thru relations.290 # they are deployed thru relations.
246 subordinates = {291 subordinates = {
247 sub for sub, machines in self.apps_to_machines.items() if machines == set()292 sub for sub, machines in self.apps_to_machines.items() if machines == set()
248 }293 }
249 for relation in self.relations_data:294 for relation in self.relations_data:
250 app_1, endpoint_1, app_2, endpoint_2 = self.split_relation(relation)295 app_1, _, app_2, _ = self.split_relation(relation)
251 # update with the machines of the application that the subordinate charm relate.296 # update with the machines of the application that the subordinate charm relate.
252 if app_1 in subordinates:297 if app_1 in subordinates:
253 self.apps_to_machines[app_1].update(self.apps_to_machines[app_2])298 sub_machines = self.apps_to_machines[app_2]
299 self.apps_to_machines[app_1].update(sub_machines)
300 for sub_machine in sub_machines:
301 self.machines_to_apps[sub_machine].add(app_1)
254 elif app_2 in subordinates:302 elif app_2 in subordinates:
303 sub_machines = self.apps_to_machines[app_1]
255 self.apps_to_machines[app_2].update(self.apps_to_machines[app_1])304 self.apps_to_machines[app_2].update(self.apps_to_machines[app_1])
305 for sub_machine in sub_machines:
306 self.machines_to_apps[sub_machine].add(app_2)
256307
257 @staticmethod308 @staticmethod
258 def sorted_machines(machine: str) -> Tuple[int, str]:309 def sorted_machines(machine: str) -> Tuple[int, str]:
@@ -303,6 +354,20 @@ class JujuBundleFile(BaseFile):
303 apps_related.add(app_1_ep_1.split(":")[0])354 apps_related.add(app_1_ep_1.split(":")[0])
304 return apps_related355 return apps_related
305356
357 def filter_lxd_on_machine(self, machine: str) -> Set:
358 """Lxd containers on a machine.
359
360 :param machine: machine id.
361 :type machine: str
362 :return: lxd containers in the machine.
363 :rtype: Set
364 """
365 return {
366 lxd_machine
367 for lxd_machine in self.machines
368 if "lxd" in lxd_machine and lxd_machine.split(":")[1] == machine
369 }
370
306371
307def input_handler(372def input_handler(
308 parsed_yaml: Dict[str, Any], applications_key: str373 parsed_yaml: Dict[str, Any], applications_key: str
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index b49a4b2..0f09bba 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -242,17 +242,31 @@ def rules_files():
242242
243243
244@pytest.fixture244@pytest.fixture
245def input_files(parsed_yaml_status, parsed_yaml_bundle):245def input_files(
246 parsed_yaml_status,
247 parsed_yaml_bundle,
248 parsed_hyper_converged_yaml_status,
249 parsed_hyper_converged_yaml_bundle,
250):
246 return {251 return {
247 "juju-status": JujuStatusFile(252 "juju-status": JujuStatusFile(
248 applications_data=parsed_yaml_status["applications"],253 applications_data=parsed_yaml_status["applications"],
249 machines_data=parsed_yaml_status["machines"],254 machines_data=parsed_yaml_status["machines"],
250 ),255 ),
256 "juju-status-hyper-converged": JujuStatusFile(
257 applications_data=parsed_hyper_converged_yaml_status["applications"],
258 machines_data=parsed_hyper_converged_yaml_status["machines"],
259 ),
251 "juju-bundle": JujuBundleFile(260 "juju-bundle": JujuBundleFile(
252 applications_data=parsed_yaml_bundle["applications"],261 applications_data=parsed_yaml_bundle["applications"],
253 machines_data=parsed_yaml_bundle["machines"],262 machines_data=parsed_yaml_bundle["machines"],
254 relations_data=parsed_yaml_bundle["relations"],263 relations_data=parsed_yaml_bundle["relations"],
255 ),264 ),
265 "juju-bundle-parsed-hyper-converged": JujuBundleFile(
266 applications_data=parsed_hyper_converged_yaml_bundle["applications"],
267 machines_data=parsed_hyper_converged_yaml_bundle["machines"],
268 relations_data=parsed_hyper_converged_yaml_bundle["relations"],
269 ),
256 }270 }
257271
258272
@@ -476,3 +490,325 @@ def parsed_yaml_bundle():
476 ],490 ],
477 ],491 ],
478 }492 }
493
494
495@pytest.fixture
496def parsed_hyper_converged_yaml_status():
497 """Representation of a hyper converged model with masakari."""
498 return {
499 "machines": {
500 "0": {
501 "juju-status": {"current": "started"},
502 "machine-status": {"current": "running"},
503 "modification-status": {
504 "current": "idle",
505 },
506 "containers": {
507 "0/lxd/0": {
508 "juju-status": {"current": "started"},
509 "machine-status": {"current": "running"},
510 "modification-status": {"current": "applied"},
511 "constraints": "arch=amd64 spaces=",
512 "hardware": "availability-zone=nova",
513 },
514 "0/lxd/1": {
515 "juju-status": {"current": "started"},
516 "machine-status": {"current": "running"},
517 "modification-status": {"current": "applied"},
518 "constraints": "arch=amd64 spaces=",
519 "hardware": "availability-zone=nova",
520 },
521 },
522 "constraints": "arch=amd64 mem=4096M",
523 "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
524 },
525 "1": {
526 "juju-status": {"current": "started"},
527 "machine-status": {"current": "running"},
528 "modification-status": {"current": "idle"},
529 "constraints": "arch=amd64 mem=4096M",
530 "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
531 },
532 "2": {
533 "juju-status": {"current": "started"},
534 "machine-status": {"current": "running"},
535 "modification-status": {"current": "idle"},
536 "constraints": "arch=amd64 mem=4096M",
537 "hardware": "arch=amd64 cores=2 mem=4096M root-disk=40960M availability-zone=nova",
538 },
539 "3": {
540 "juju-status": {"current": "started"},
541 "machine-status": {"current": "running"},
542 "modification-status": {"current": "idle"},
543 "constraints": "arch=amd64",
544 "hardware": "arch=amd64 cores=1 mem=2048M root-disk=20480M availability-zone=nova",
545 },
546 },
547 "applications": {
548 "ceilometer": {
549 "charm": "ceilometer",
550 "charm-name": "ceilometer",
551 "application-status": {"current": "active"},
552 "relations": {"cluster": ["ceilometer"]},
553 "units": {
554 "ceilometer/0": {
555 "workload-status": {"current": "active"},
556 "juju-status": {"current": "idle"},
557 "machine": "0/lxd/0",
558 }
559 },
560 "endpoint-bindings": {
561 "": "alpha",
562 "admin": "alpha",
563 "amqp": "alpha",
564 "amqp-listener": "alpha",
565 "ceilometer-service": "alpha",
566 "certificates": "alpha",
567 "cluster": "alpha",
568 "event-service": "alpha",
569 "ha": "alpha",
570 "identity-credentials": "alpha",
571 "identity-notifications": "alpha",
572 "identity-service": "alpha",
573 "internal": "alpha",
574 "metric-service": "alpha",
575 "nrpe-external-master": "alpha",
576 "public": "alpha",
577 "shared-db": "alpha",
578 },
579 },
580 "ceph-mon": {
581 "charm": "ceph-mon",
582 "charm-name": "ceph-mon",
583 "application-status": {"current": "active"},
584 "relations": {
585 "client": ["nova-compute"],
586 "mon": ["ceph-mon"],
587 "osd": ["ceph-osd"],
588 },
589 "units": {
590 "ceph-mon/0": {
591 "workload-status": {"current": "idle"},
592 "juju-status": {"current": "idle"},
593 "machine": "0",
594 },
595 "ceph-mon/1": {
596 "workload-status": {"current": "idle"},
597 "juju-status": {"current": "idle"},
598 "machine": "1",
599 },
600 "ceph-mon/2": {
601 "workload-status": {"current": "idle"},
602 "juju-status": {"current": "idle"},
603 "machine": "2",
604 },
605 },
606 "endpoint-bindings": {
607 "": "alpha",
608 "admin": "alpha",
609 "bootstrap-source": "alpha",
610 "client": "alpha",
611 "cluster": "alpha",
612 "dashboard": "alpha",
613 "mds": "alpha",
614 "mon": "alpha",
615 "nrpe-external-master": "alpha",
616 "osd": "alpha",
617 "prometheus": "alpha",
618 "public": "alpha",
619 "radosgw": "alpha",
620 "rbd-mirror": "alpha",
621 },
622 },
623 "ceph-osd": {
624 "charm": "ceph-osd",
625 "charm-name": "ceph-osd",
626 "application-status": {"current": "active"},
627 "relations": {"mon": ["ceph-mon"]},
628 "units": {
629 "ceph-osd/0": {
630 "workload-status": {"current": "idle"},
631 "juju-status": {"current": "idle"},
632 "machine": "0",
633 },
634 "ceph-osd/1": {
635 "workload-status": {"current": "idle"},
636 "juju-status": {"current": "idle"},
637 "machine": "1",
638 },
639 "ceph-osd/2": {
640 "workload-status": {"current": "idle"},
641 "juju-status": {"current": "idle"},
642 "machine": "2",
643 },
644 },
645 "endpoint-bindings": {
646 "": "alpha",
647 "cluster": "alpha",
648 "mon": "alpha",
649 "nrpe-external-master": "alpha",
650 "public": "alpha",
651 "secrets-storage": "alpha",
652 },
653 },
654 "heat": {
655 "charm": "heat",
656 "series": "focal",
657 "charm-name": "heat",
658 "application-status": {"current": "active"},
659 "relations": {"cluster": ["heat"]},
660 "units": {
661 "heat/0": {
662 "workload-status": {"current": "idle"},
663 "juju-status": {"current": "idle"},
664 "machine": "0/lxd/1",
665 }
666 },
667 "endpoint-bindings": {
668 "": "alpha",
669 "admin": "alpha",
670 "amqp": "alpha",
671 "certificates": "alpha",
672 "cluster": "alpha",
673 "ha": "alpha",
674 "heat-plugin-subordinate": "alpha",
675 "identity-service": "alpha",
676 "internal": "alpha",
677 "nrpe-external-master": "alpha",
678 "public": "alpha",
679 "shared-db": "alpha",
680 },
681 },
682 "masakari": {
683 "charm": "masakari",
684 "charm-name": "masakari",
685 "application-status": {"current": "active"},
686 "relations": {"cluster": ["masakari"]},
687 "units": {
688 "masakari/0": {
689 "workload-status": {"current": "idle"},
690 "juju-status": {"current": "idle"},
691 "machine": "3",
692 }
693 },
694 "endpoint-bindings": {
695 "": "alpha",
696 "admin": "alpha",
697 "amqp": "alpha",
698 "certificates": "alpha",
699 "cluster": "alpha",
700 "ha": "alpha",
701 "identity-service": "alpha",
702 "internal": "alpha",
703 "public": "alpha",
704 "shared-db": "alpha",
705 },
706 },
707 "nova-compute": {
708 "charm": "nova-compute",
709 "charm-name": "nova-compute",
710 "application-status": {"current": "active"},
711 "relations": {"ceph": ["ceph-mon"], "compute-peer": ["nova-compute"]},
712 "units": {
713 "nova-compute/0": {
714 "workload-status": {"current": "idle"},
715 "juju-status": {"current": "idle"},
716 "machine": "0",
717 },
718 "nova-compute/1": {
719 "workload-status": {"current": "idle"},
720 "juju-status": {"current": "idle"},
721 "machine": "1",
722 },
723 "nova-compute/2": {
724 "workload-status": {"current": "idle"},
725 "juju-status": {"current": "idle"},
726 "machine": "2",
727 },
728 },
729 "endpoint-bindings": {
730 "": "alpha",
731 "amqp": "alpha",
732 "ceph": "alpha",
733 "ceph-access": "alpha",
734 "cloud-compute": "alpha",
735 "cloud-credentials": "alpha",
736 "compute-peer": "alpha",
737 "ephemeral-backend": "alpha",
738 "image-service": "alpha",
739 "internal": "alpha",
740 "ironic-api": "alpha",
741 "lxd": "alpha",
742 "migration": "alpha",
743 "neutron-plugin": "alpha",
744 "nova-ceilometer": "alpha",
745 "nrpe-external-master": "alpha",
746 "secrets-storage": "alpha",
747 },
748 },
749 },
750 }
751
752
753@pytest.fixture
754def parsed_hyper_converged_yaml_bundle():
755 """Representation of a hyper converged model with masakari."""
756 return {
757 "series": "focal",
758 "applications": {
759 "ceilometer": {
760 "charm": "ceilometer",
761 "num_units": 1,
762 "to": ["lxd:0"],
763 "constraints": "arch=amd64",
764 },
765 "ceph-mon": {
766 "charm": "ceph-mon",
767 "num_units": 3,
768 "to": ["0", "1", "2"],
769 "constraints": "arch=amd64",
770 },
771 "ceph-osd": {
772 "charm": "ceph-osd",
773 "num_units": 3,
774 "to": ["0", "1", "2"],
775 "constraints": "arch=amd64 mem=4096",
776 "storage": {
777 "bluestore-db": "loop,0,1024",
778 "bluestore-wal": "loop,0,1024",
779 "osd-devices": "loop,0,1024",
780 "osd-journals": "loop,0,1024",
781 },
782 },
783 "heat": {
784 "charm": "heat",
785 "resources": {"policyd-override": 0},
786 "num_units": 1,
787 "to": ["lxd:0"],
788 "constraints": "arch=amd64",
789 },
790 "masakari": {
791 "charm": "masakari",
792 "num_units": 1,
793 "to": ["3"],
794 "constraints": "arch=amd64",
795 },
796 "nova-compute": {
797 "charm": "nova-compute",
798 "num_units": 3,
799 "to": ["0", "1", "2"],
800 "constraints": "arch=amd64",
801 "storage": {"ephemeral-device": "loop,0,10240"},
802 },
803 },
804 "machines": {
805 "0": {"constraints": "arch=amd64 mem=4096"},
806 "1": {"constraints": "arch=amd64 mem=4096"},
807 "2": {"constraints": "arch=amd64 mem=4096"},
808 "3": {"constraints": "arch=amd64"},
809 },
810 "relations": [
811 ["ceph-mon:client", "nova-compute:ceph"],
812 ["ceph-mon:osd", "ceph-osd:mon"],
813 ],
814 }
diff --git a/tests/unit/test_hyper_converged.py b/tests/unit/test_hyper_converged.py
479new file mode 100644815new file mode 100644
index 0000000..751af7a
--- /dev/null
+++ b/tests/unit/test_hyper_converged.py
@@ -0,0 +1,32 @@
1from collections import defaultdict
2
3import pytest
4
5from jujulint.checks import hyper_converged
6
7
8@pytest.mark.parametrize(
9 "masakari, input_file_type",
10 [
11 (True, "juju-status-hyper-converged"),
12 (False, "juju-status-hyper-converged"),
13 (True, "juju-bundle-parsed-hyper-converged"),
14 (False, "juju-bundle-parsed-hyper-converged"),
15 ],
16)
17def test_check_hyper_converged(input_files, masakari, input_file_type):
18 """Test hyper_converged models."""
19 input_file = input_files[input_file_type]
20 expected_result = defaultdict(lambda: defaultdict(set))
21 if masakari and "juju-status" in input_file_type:
22 expected_result["0"]["0/lxd/0"] = {"ceilometer"}
23 expected_result["0"]["0/lxd/1"] = {"heat"}
24 elif masakari and "juju-bundle" in input_file_type:
25 expected_result["0"]["lxd:0"] = {"ceilometer", "heat"}
26 else:
27 # remove masakari from input file
28 del input_file.applications_data["masakari"]
29 del input_file.machines_data["3"]
30 input_file.charms = set()
31 input_file.map_file()
32 assert hyper_converged.check_hyper_converged(input_file) == expected_result
diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py
index 682b664..f06ff4b 100644
--- a/tests/unit/test_input.py
+++ b/tests/unit/test_input.py
@@ -51,6 +51,18 @@ def test_file_inputs(input_files, input_file_type):
51 "keystone": {"lxd:1"},51 "keystone": {"lxd:1"},
52 },52 },
53 },53 },
54 "machines_to_apps": {
55 "juju-status": {
56 "0": {"nrpe-host", "elasticsearch"},
57 "1": {"ubuntu", "nrpe-host"},
58 "1/lxd/0": {"nrpe-container", "keystone"},
59 },
60 "juju-bundle": {
61 "0": {"nrpe-host", "elasticsearch"},
62 "1": {"ubuntu", "nrpe-host"},
63 "lxd:1": {"nrpe-container", "keystone"},
64 },
65 },
54 }66 }
55 assert input_file.applications == expected_output["applications"]67 assert input_file.applications == expected_output["applications"]
56 assert input_file.machines == expected_output["machines"][input_file_type]68 assert input_file.machines == expected_output["machines"][input_file_type]
@@ -58,6 +70,10 @@ def test_file_inputs(input_files, input_file_type):
58 input_file.apps_to_machines70 input_file.apps_to_machines
59 == expected_output["apps_to_machines"][input_file_type]71 == expected_output["apps_to_machines"][input_file_type]
60 )72 )
73 assert (
74 input_file.machines_to_apps
75 == expected_output["machines_to_apps"][input_file_type]
76 )
61 assert input_file.charms == expected_output["charms"]77 assert input_file.charms == expected_output["charms"]
62 assert input_file.app_to_charm == expected_output["app_to_charm"]78 assert input_file.app_to_charm == expected_output["app_to_charm"]
63 assert input_file.charm_to_app == expected_output["charm_to_app"]79 assert input_file.charm_to_app == expected_output["charm_to_app"]
@@ -207,6 +223,48 @@ def test_input_handler(parsed_yaml, expected_output, request):
207 )223 )
208224
209225
226@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
227def test_filter_machines_by_charm(input_files, input_file_type):
228 """Test filter_machines_by_charm method."""
229 input_file = input_files[input_file_type]
230 if input_file_type == "juju-status":
231 expected_output = {
232 "nrpe": {"0", "1", "1/lxd/0"},
233 "keystone": {"1/lxd/0"},
234 "ubuntu": {"1"},
235 "elasticsearch": {"0"},
236 }
237 else:
238 expected_output = {
239 "nrpe": {"0", "1", "lxd:1"},
240 "keystone": {"lxd:1"},
241 "ubuntu": {"1"},
242 "elasticsearch": {"0"},
243 }
244 for charm in input_file.charms:
245 assert input_file.filter_machines_by_charm(charm) == expected_output[charm]
246
247
248@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
249def test_filter_lxd_on_machine(input_files, input_file_type):
250 """Test filter_lxd_on_machine method."""
251 input_file = input_files[input_file_type]
252 if input_file_type == "juju-status":
253 expected_output = {
254 "0": set(),
255 "1": {"1/lxd/0"},
256 "1/lxd/0": set(),
257 }
258 else:
259 expected_output = {
260 "0": set(),
261 "1": {"lxd:1"},
262 "lxd:1": set(),
263 }
264 for machine in input_file.machines:
265 assert input_file.filter_lxd_on_machine(machine) == expected_output[machine]
266
267
210def test_raise_not_implemented_methods(parsed_yaml_status):268def test_raise_not_implemented_methods(parsed_yaml_status):
211 # declare a new input class269 # declare a new input class
212 @dataclass270 @dataclass
@@ -231,3 +289,6 @@ def test_raise_not_implemented_methods(parsed_yaml_status):
231289
232 with pytest.raises(NotImplementedError):290 with pytest.raises(NotImplementedError):
233 new_input.sorted_machines("0")291 new_input.sorted_machines("0")
292
293 with pytest.raises(NotImplementedError):
294 new_input.filter_lxd_on_machine("0")
diff --git a/tests/unit/test_jujulint.py b/tests/unit/test_jujulint.py
index 77dbb5e..c87722d 100644
--- a/tests/unit/test_jujulint.py
+++ b/tests/unit/test_jujulint.py
@@ -7,7 +7,8 @@ from unittest import mock
7import pytest7import pytest
8import yaml8import yaml
99
10from jujulint import check_spaces, lint, relations10from jujulint import lint
11from jujulint.checks import relations, spaces
11from jujulint.lint import VALID_LOG_LEVEL12from jujulint.lint import VALID_LOG_LEVEL
1213
1314
@@ -1424,7 +1425,7 @@ applications:
1424 This warning should be triggerred if some applications have bindings and some1425 This warning should be triggerred if some applications have bindings and some
1425 dont.1426 dont.
1426 """1427 """
1427 logger_mock = mocker.patch.object(check_spaces, "LOGGER")1428 logger_mock = mocker.patch.object(spaces, "LOGGER")
14281429
1429 app_without_binding = "prometheus-app"1430 app_without_binding = "prometheus-app"
1430 bundle = {1431 bundle = {
@@ -1458,7 +1459,7 @@ applications:
1458 mentioned explicitly will be bound to this default space.1459 mentioned explicitly will be bound to this default space.
1459 Juju lint should raise warning if bundles do not define default space.1460 Juju lint should raise warning if bundles do not define default space.
1460 """1461 """
1461 logger_mock = mocker.patch.object(check_spaces, "LOGGER")1462 logger_mock = mocker.patch.object(spaces, "LOGGER")
1462 app_without_default_space = "telegraf-app"1463 app_without_default_space = "telegraf-app"
14631464
1464 bundle = {1465 bundle = {
@@ -1490,7 +1491,7 @@ applications:
14901491
1491 def test_check_spaces_multi_model_warning(self, linter, mocker):1492 def test_check_spaces_multi_model_warning(self, linter, mocker):
1492 """Test that check_spaces shows warning if some application are from another model."""1493 """Test that check_spaces shows warning if some application are from another model."""
1493 logger_mock = mocker.patch.object(check_spaces, "LOGGER")1494 logger_mock = mocker.patch.object(spaces, "LOGGER")
14941495
1495 app_another_model = "prometheus-app"1496 app_another_model = "prometheus-app"
1496 bundle = {1497 bundle = {
@@ -1745,3 +1746,60 @@ applications:
1745 logger_mock.assert_has_calls(1746 logger_mock.assert_has_calls(
1746 [mocker.call(expected_message, level=logging.ERROR)]1747 [mocker.call(expected_message, level=logging.ERROR)]
1747 )1748 )
1749
1750 @pytest.mark.parametrize(
1751 "input_file_type",
1752 ["juju-status-hyper-converged", "juju-bundle-parsed-hyper-converged"],
1753 )
1754 def test_check_hyper_converged(self, linter, input_files, mocker, input_file_type):
1755 """Test check_hyper_converged."""
1756 input_file = input_files[input_file_type]
1757 mock_message_handler = mocker.patch("jujulint.lint.Linter.message_handler")
1758 msg = (
1759 "Deployment has Masakari and the machine: '{}' "
1760 "has nova/osd and the lxd: '{}' with those services {}"
1761 )
1762 expected_output = [
1763 mocker.call(
1764 {
1765 "id": "hyper-converged-masakari",
1766 "tags": ["hyper-converged", "masakari"],
1767 "message": msg.format(
1768 "0",
1769 "lxd:0",
1770 ["ceilometer", "heat"],
1771 ),
1772 },
1773 log_level=logging.WARNING,
1774 )
1775 ]
1776 if "juju-status" in input_file_type:
1777 expected_output = [
1778 mocker.call(
1779 {
1780 "id": "hyper-converged-masakari",
1781 "tags": ["hyper-converged", "masakari"],
1782 "message": msg.format(
1783 "0",
1784 "0/lxd/0",
1785 ["ceilometer"],
1786 ),
1787 },
1788 log_level=logging.WARNING,
1789 ),
1790 mocker.call(
1791 {
1792 "id": "hyper-converged-masakari",
1793 "tags": ["hyper-converged", "masakari"],
1794 "message": msg.format(
1795 "0",
1796 "0/lxd/1",
1797 ["heat"],
1798 ),
1799 },
1800 log_level=logging.WARNING,
1801 ),
1802 ]
1803
1804 linter.check_hyper_converged(input_file)
1805 mock_message_handler.assert_has_calls(expected_output, any_order=True)
diff --git a/tests/unit/test_relations.py b/tests/unit/test_relations.py
index f13d544..121fe9e 100644
--- a/tests/unit/test_relations.py
+++ b/tests/unit/test_relations.py
@@ -2,7 +2,7 @@
2"""Test the relations module."""2"""Test the relations module."""
3import pytest3import pytest
44
5from jujulint import relations5from jujulint.checks import relations
66
7CHARM_TO_APP = {"nrpe-host", "nrpe-container"}7CHARM_TO_APP = {"nrpe-host", "nrpe-container"}
8CHARM = "nrpe"8CHARM = "nrpe"
@@ -352,7 +352,7 @@ def test_relations_raise_not_implemented(input_files, mocker):
352 """Ensure that a new class that not implement mandatory methods raises error."""352 """Ensure that a new class that not implement mandatory methods raises error."""
353 logger_mock = mocker.patch.object(relations, "LOGGER")353 logger_mock = mocker.patch.object(relations, "LOGGER")
354 mocker.patch(354 mocker.patch(
355 "jujulint.relations.RelationRule.relation_exist_check",355 "jujulint.checks.relations.RelationRule.relation_exist_check",
356 side_effect=NotImplementedError(),356 side_effect=NotImplementedError(),
357 )357 )
358 input_file = input_files["juju-status"]358 input_file = input_files["juju-status"]
diff --git a/tests/unit/test_check_spaces.py b/tests/unit/test_spaces.py
359similarity index 80%359similarity index 80%
360rename from tests/unit/test_check_spaces.py360rename from tests/unit/test_check_spaces.py
361rename to tests/unit/test_spaces.py361rename to tests/unit/test_spaces.py
index 9b77e66..db16505 100644
--- a/tests/unit/test_check_spaces.py
+++ b/tests/unit/test_spaces.py
@@ -1,9 +1,9 @@
1"""Tests for check_spaces.py module."""1"""Tests for spaces.py module."""
2from unittest.mock import call2from unittest.mock import call
33
4import pytest4import pytest
55
6from jujulint import check_spaces6from jujulint.checks import spaces
77
88
9def test_relation_init():9def test_relation_init():
@@ -11,7 +11,7 @@ def test_relation_init():
11 ep_1 = "Endpoint 1"11 ep_1 = "Endpoint 1"
12 ep_2 = "Endpoint 2"12 ep_2 = "Endpoint 2"
1313
14 relation = check_spaces.Relation(ep_1, ep_2)14 relation = spaces.Relation(ep_1, ep_2)
1515
16 assert relation.endpoint1 == ep_116 assert relation.endpoint1 == ep_1
17 assert relation.endpoint2 == ep_217 assert relation.endpoint2 == ep_2
@@ -23,7 +23,7 @@ def test_relation_str():
23 ep_2 = "Endpoint 2"23 ep_2 = "Endpoint 2"
24 expected_str = "Relation({} - {})".format(ep_1, ep_2)24 expected_str = "Relation({} - {})".format(ep_1, ep_2)
2525
26 relation = check_spaces.Relation(ep_1, ep_2)26 relation = spaces.Relation(ep_1, ep_2)
2727
28 assert str(relation) == expected_str28 assert str(relation) == expected_str
2929
@@ -63,8 +63,8 @@ def test_relation_str():
63)63)
64def test_relation_eq(rel_1_ep_1, rel_1_ep_2, rel_2_ep_1, rel_2_ep_2, expected_result):64def test_relation_eq(rel_1_ep_1, rel_1_ep_2, rel_2_ep_1, rel_2_ep_2, expected_result):
65 """Test equality operator of Relation class. Only return true if both endpoints match."""65 """Test equality operator of Relation class. Only return true if both endpoints match."""
66 relation_1 = check_spaces.Relation(rel_1_ep_1, rel_1_ep_2)66 relation_1 = spaces.Relation(rel_1_ep_1, rel_1_ep_2)
67 relation_2 = check_spaces.Relation(rel_2_ep_1, rel_2_ep_2)67 relation_2 = spaces.Relation(rel_2_ep_1, rel_2_ep_2)
6868
69 assert (relation_1 == relation_2) == expected_result69 assert (relation_1 == relation_2) == expected_result
7070
@@ -74,7 +74,7 @@ def test_relation_endpoints_prop():
74 ep_1 = "Endpoint 1"74 ep_1 = "Endpoint 1"
75 ep_2 = "Endpoint 2"75 ep_2 = "Endpoint 2"
7676
77 relation = check_spaces.Relation(ep_1, ep_2)77 relation = spaces.Relation(ep_1, ep_2)
7878
79 assert relation.endpoints == [ep_1, ep_2]79 assert relation.endpoints == [ep_1, ep_2]
8080
@@ -105,7 +105,7 @@ def test_space_mismatch_init(input_order, output_order):
105 This test also verifies that spaces in SpaceMismatch instance are ordered105 This test also verifies that spaces in SpaceMismatch instance are ordered
106 alphabetically based on the endpoint name.106 alphabetically based on the endpoint name.
107 """107 """
108 mismatch_instance = check_spaces.SpaceMismatch(*input_order)108 mismatch_instance = spaces.SpaceMismatch(*input_order)
109109
110 # Assert that endpoints are alphabetically reordered110 # Assert that endpoints are alphabetically reordered
111 assert mismatch_instance.endpoint1 == output_order[0]111 assert mismatch_instance.endpoint1 == output_order[0]
@@ -124,7 +124,7 @@ def test_space_mismatch_str():
124 ep_1, space_1, ep_2, space_2124 ep_1, space_1, ep_2, space_2
125 )125 )
126126
127 mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)127 mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
128128
129 assert str(mismatch_instance) == expected_str129 assert str(mismatch_instance) == expected_str
130130
@@ -136,9 +136,9 @@ def test_space_mismatch_relation_prop():
136 space_1 = "Space 1"136 space_1 = "Space 1"
137 space_2 = "Space 2"137 space_2 = "Space 2"
138138
139 expected_relation = check_spaces.Relation(ep_1, ep_2)139 expected_relation = spaces.Relation(ep_1, ep_2)
140140
141 mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)141 mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
142142
143 assert mismatch_instance.relation == expected_relation143 assert mismatch_instance.relation == expected_relation
144144
@@ -156,9 +156,9 @@ def test_space_mismatch_get_charm_relation():
156156
157 app_map = {app_1: charm_1, app_2: charm_2}157 app_map = {app_1: charm_1, app_2: charm_2}
158158
159 expected_relation = check_spaces.Relation("ubuntu:endpoint_1", "nrpe:endpoint_2")159 expected_relation = spaces.Relation("ubuntu:endpoint_1", "nrpe:endpoint_2")
160160
161 mismatch_instance = check_spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)161 mismatch_instance = spaces.SpaceMismatch(ep_1, space_1, ep_2, space_2)
162162
163 assert mismatch_instance.get_charm_relation(app_map) == expected_relation163 assert mismatch_instance.get_charm_relation(app_map) == expected_relation
164164
@@ -173,32 +173,28 @@ def test_find_space_mismatches(use_cmr, mocker):
173 space_2 = "space 2"173 space_2 = "space 2"
174 app_endpoint_1 = app_1 + ":endpoint"174 app_endpoint_1 = app_1 + ":endpoint"
175 app_endpoint_2 = app_2 + ":endpoint"175 app_endpoint_2 = app_2 + ":endpoint"
176 relation = check_spaces.Relation(176 relation = spaces.Relation(app_endpoint_1, "XModel" if use_cmr else app_endpoint_2)
177 app_endpoint_1, "XModel" if use_cmr else app_endpoint_2
178 )
179 app_list = [app_1, app_2]177 app_list = [app_1, app_2]
180 app_spaces = {app_1: {space_1: "foo"}, app_2: {space_2: "bar"}}178 app_spaces = {app_1: {space_1: "foo"}, app_2: {space_2: "bar"}}
181179
182 app_list_mock = mocker.patch.object(180 app_list_mock = mocker.patch.object(
183 check_spaces, "get_juju_applications", return_value=app_list181 spaces, "get_juju_applications", return_value=app_list
184 )182 )
185 app_spaces_mock = mocker.patch.object(183 app_spaces_mock = mocker.patch.object(
186 check_spaces, "get_application_spaces", return_value=app_spaces184 spaces, "get_application_spaces", return_value=app_spaces
187 )185 )
188 rel_list_mock = mocker.patch.object(186 rel_list_mock = mocker.patch.object(
189 check_spaces, "get_application_relations", return_value=[relation]187 spaces, "get_application_relations", return_value=[relation]
190 )188 )
191 rel_space_mock = mocker.patch.object(189 rel_space_mock = mocker.patch.object(
192 check_spaces, "get_relation_space", side_effect=[space_1, space_2]190 spaces, "get_relation_space", side_effect=[space_1, space_2]
193 )191 )
194192
195 expected_mismatch = [193 expected_mismatch = [
196 check_spaces.SpaceMismatch(194 spaces.SpaceMismatch(relation.endpoint1, space_1, relation.endpoint2, space_2)
197 relation.endpoint1, space_1, relation.endpoint2, space_2
198 )
199 ]195 ]
200196
201 mismatch = check_spaces.find_space_mismatches(sample_yaml, True)197 mismatch = spaces.find_space_mismatches(sample_yaml, True)
202 result_pairs = zip(expected_mismatch, mismatch)198 result_pairs = zip(expected_mismatch, mismatch)
203199
204 app_list_mock.assert_called_once_with(sample_yaml)200 app_list_mock.assert_called_once_with(sample_yaml)
@@ -224,7 +220,7 @@ def test_get_juju_applications():
224220
225 expected_apps = [app_1, app_2]221 expected_apps = [app_1, app_2]
226222
227 apps = check_spaces.get_juju_applications(sample_yaml)223 apps = spaces.get_juju_applications(sample_yaml)
228224
229 assert apps == expected_apps225 assert apps == expected_apps
230226
@@ -235,7 +231,7 @@ def test_get_application_spaces(mocker):
235 This test also verifies that default binding to space "alpha" is added to applications231 This test also verifies that default binding to space "alpha" is added to applications
236 that do not specify any bindings.232 that do not specify any bindings.
237 """233 """
238 logger_mock = mocker.patch.object(check_spaces, "LOGGER")234 logger_mock = mocker.patch.object(spaces, "LOGGER")
239 default_binding = ""235 default_binding = ""
240 default_space = "custom_default_space"236 default_space = "custom_default_space"
241 public_binding = "public"237 public_binding = "public"
@@ -269,7 +265,7 @@ def test_get_application_spaces(mocker):
269 app_list[2]: {default_binding: "alpha"},265 app_list[2]: {default_binding: "alpha"},
270 }266 }
271267
272 app_spaces = check_spaces.get_application_spaces(app_list, sample_yaml)268 app_spaces = spaces.get_application_spaces(app_list, sample_yaml)
273269
274 # Verify that all the bindings for properly defined app were returned270 # Verify that all the bindings for properly defined app were returned
275 # Verify that default binding was added to app that did not have any bindings defined271 # Verify that default binding was added to app that did not have any bindings defined
@@ -298,11 +294,11 @@ def test_get_application_relations():
298 }294 }
299295
300 expected_relations = [296 expected_relations = [
301 check_spaces.Relation("ubuntu:juju-info", "nrpe:general-info"),297 spaces.Relation("ubuntu:juju-info", "nrpe:general-info"),
302 check_spaces.Relation("vault:shared-db", "mysql-innodb-cluster:shared-db"),298 spaces.Relation("vault:shared-db", "mysql-innodb-cluster:shared-db"),
303 ]299 ]
304300
305 relations = check_spaces.get_application_relations(sample_yaml)301 relations = spaces.get_application_relations(sample_yaml)
306302
307 assert relations == expected_relations303 assert relations == expected_relations
308304
@@ -323,21 +319,21 @@ def test_get_relation_space(use_explicit_binding):
323 else:319 else:
324 expected_space = default_space320 expected_space = default_space
325321
326 space = check_spaces.get_relation_space(endpoint, app_spaces)322 space = spaces.get_relation_space(endpoint, app_spaces)
327323
328 assert space == expected_space324 assert space == expected_space
329325
330326
331def test_get_relation_space_cmr(mocker):327def test_get_relation_space_cmr(mocker):
332 """Test getting space for cross model relation."""328 """Test getting space for cross model relation."""
333 logger_mock = mocker.patch.object(check_spaces, "LOGGER")329 logger_mock = mocker.patch.object(spaces, "LOGGER")
334 app_name = "ubuntu"330 app_name = "ubuntu"
335 interface = "juju_info"331 interface = "juju_info"
336 endpoint = app_name + ":" + interface332 endpoint = app_name + ":" + interface
337333
338 app_spaces = {}334 app_spaces = {}
339335
340 space = check_spaces.get_relation_space(endpoint, app_spaces)336 space = spaces.get_relation_space(endpoint, app_spaces)
341337
342 assert space == "XModel"338 assert space == "XModel"
343 logger_mock.warning.assert_called_once_with(339 logger_mock.warning.assert_called_once_with(
diff --git a/tox.ini b/tox.ini
index 4dea114..08183e2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -23,7 +23,7 @@ deps =
23 -r{toxinidir}/requirements.txt23 -r{toxinidir}/requirements.txt
24 -r{toxinidir}/tests/unit/requirements.txt24 -r{toxinidir}/tests/unit/requirements.txt
25commands =25commands =
26 pytest -v \26 pytest -vv \
27 --cov=jujulint \27 --cov=jujulint \
28 --new-first \28 --new-first \
29 --last-failed \29 --last-failed \

Subscribers

People subscribed via source and target branches