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

Proposed by Gabriel Cocenza
Status: Merged
Approved by: Martin Kalcok
Approved revision: 5dc4c256dedc2d5596d00f1c6be6453df5a572a9
Merged at revision: 9160dae8189345db8494835da2465d9acf8b9d8f
Proposed branch: ~gabrielcocenza/juju-lint:bug/1893272
Merge into: juju-lint:master
Prerequisite: ~gabrielcocenza/juju-lint:bug/1965762-B
Diff against target: 1965 lines (+1241/-287)
10 files modified
CONTRIBUTING.md (+2/-2)
contrib/includes/base.yaml (+15/-0)
jujulint/lint.py (+20/-9)
jujulint/model_input.py (+331/-0)
jujulint/relations.py (+73/-110)
setup.py (+1/-1)
tests/unit/conftest.py (+224/-67)
tests/unit/test_input.py (+233/-0)
tests/unit/test_jujulint.py (+72/-15)
tests/unit/test_relations.py (+270/-83)
Reviewer Review Type Date Requested Status
🤖 prod-jenkaas-bootstack (community) continuous-integration Approve
Martin Kalcok (community) Approve
Mert Kirpici Pending
BootStack Reviewers Pending
Eric Chen Pending
Review via email: mp+429919@code.launchpad.net

Commit message

added new relation rule check for ubiquitous

- ubiquitous relation rule to ensure every machine has a charm unit on it.

- added ubiquitous relation rule to nrpe, telegraf, filebeat, landscape-client and ubuntu-advantage.

- added a new model input module that is responsible to differentiate the
  type of input (Juju Status, Juju Bundle). The input object encapsulate
  all mapping and queries necessary against a file.

Closes-Bug: #1893272

Description of the change

This change is mainly focused on checking charms that need to be deployed on every machine.

The next step is differentiate container, virtual and physical machines and add adapt to check ntp, hw-health, lldpd just on the physical ones.

To post a comment you must log in.
Revision history for this message
Gabriel Cocenza (gabrielcocenza) :
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
Gabriel Cocenza (gabrielcocenza) wrote :

logs of unit, lint and functional test [0]

[0] https://pastebin.canonical.com/p/8Vp9Vdv2xQ/

Revision history for this message
Martin Kalcok (martin-kalcok) wrote :

Thanks for the improvements. Overall LGTM, but I do have some comments (see inline)

review: Needs Fixing
Revision history for this message
Gabriel Cocenza (gabrielcocenza) :
Revision history for this message
Gabriel Cocenza (gabrielcocenza) :
Revision history for this message
Martin Kalcok (martin-kalcok) :
Revision history for this message
Martin Kalcok (martin-kalcok) wrote :

LGTM, thanks.

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

Change successfully merged at revision 9160dae8189345db8494835da2465d9acf8b9d8f

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ef0a32c..2baa385 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -55,9 +55,9 @@ make clean # clean the snapcraft lxd containers and the snap files cre
55```55```
56### Functional Tests56### Functional Tests
5757
58`make functional` will build the snap, rename it, install it locally and run the tests against the installed snap package. Since this action involves installing a snap package, passwordless `sudo` privileges are needed. 58`make functional` will build the snap, rename it, install it locally and run the tests against the installed snap package. Since this action involves installing a snap package, passwordless `sudo` privileges are needed.
59However for development purposes, the testing infrastructure also allows for installing `juju-lint` as a python package to a tox environment59However for development purposes, the testing infrastructure also allows for installing `juju-lint` as a python package to a tox environment
60and running tests against it. 60and running tests against it.
61In order to invoke:61In order to invoke:
62- development smoke suite, which deselects tests running against a live lxd cloud62- development smoke suite, which deselects tests running against a live lxd cloud
63 - `$ tox -e func-smoke`63 - `$ tox -e func-smoke`
diff --git a/contrib/includes/base.yaml b/contrib/includes/base.yaml
index 2ab4712..8e0a2ce 100644
--- a/contrib/includes/base.yaml
+++ b/contrib/includes/base.yaml
@@ -46,7 +46,22 @@ subordinates:
4646
47# openstack and k8s should check nrpe relations. See LP#196576247# openstack and k8s should check nrpe relations. See LP#1965762
48relations base check: &relations-base-check48relations base check: &relations-base-check
49 # NOTE(gabrielcocenza) filebeat, telegraf, nrpe are necessary until switch to the COS
49 - charm: nrpe50 - charm: nrpe
50 check: [51 check: [
51 ["*:nrpe-external-master", "nrpe:nrpe-external-master"]52 ["*:nrpe-external-master", "nrpe:nrpe-external-master"]
52 ]53 ]
54 # every machine has nrpe unit on it. See LP#1893272
55 ubiquitous: true
56
57 - charm: telegraf
58 ubiquitous: true
59
60 - charm: filebeat
61 ubiquitous: true
62
63 - charm: landscape-client
64 ubiquitous: true
65
66 - charm: ubuntu-advantage
67 ubiquitous: true
diff --git a/jujulint/lint.py b/jujulint/lint.py
index 937733e..8bbcff4 100755
--- a/jujulint/lint.py
+++ b/jujulint/lint.py
@@ -36,6 +36,7 @@ from dateutil import relativedelta
36import jujulint.util as utils36import jujulint.util as utils
37from jujulint.check_spaces import Relation, find_space_mismatches37from jujulint.check_spaces import Relation, find_space_mismatches
38from jujulint.logging import Logger38from jujulint.logging import Logger
39from jujulint.model_input import input_handler
39from jujulint.relations import RelationError, RelationsRulesBootStrap40from jujulint.relations import RelationError, RelationsRulesBootStrap
4041
41VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte", "search")42VALID_CONFIG_CHECKS = ("isset", "eq", "neq", "gte", "search")
@@ -613,20 +614,19 @@ class Linter:
613 if not self.model.extraneous_subs[sub]:614 if not self.model.extraneous_subs[sub]:
614 del self.model.extraneous_subs[sub]615 del self.model.extraneous_subs[sub]
615616
616 def check_relations(self, applications):617 def check_relations(self, input_file):
617 """Check the relations in the rules file.618 """Check the relations in the rules file.
618619
619 :param applications: applications present in the model620 :param input_file: mapped content of the input file.
620 :type applications: Dict621 :type input_file: Union[JujuBundleFile, JujuStatusFile]
621 """622 """
622 if "relations" not in self.lint_rules:623 if "relations" not in self.lint_rules:
623 self._log_with_header("No relation rules found. Skipping relation checks")624 self._log_with_header("No relation rules found. Skipping relation checks")
624 return625 return
625 try:626 try:
626 relations_rules = RelationsRulesBootStrap(627 relations_rules = RelationsRulesBootStrap(
627 charm_to_app=self.model.charm_to_app,
628 relations_rules=self.lint_rules["relations"],628 relations_rules=self.lint_rules["relations"],
629 applications=applications,629 input_file=input_file,
630 ).check()630 ).check()
631 except RelationError as e:631 except RelationError as e:
632 relations_rules = []632 relations_rules = []
@@ -656,6 +656,18 @@ class Linter:
656 }656 }
657 )657 )
658658
659 if rule.missing_machines:
660 self.handle_error(
661 {
662 "id": "missing-machine",
663 "tags": ["missing", "machine"],
664 "message": "Charm '{}' missing on machines: {}".format(
665 rule.charm,
666 rule.missing_machines,
667 ),
668 }
669 )
670
659 def check_charms_ops_mandatory(self, charm):671 def check_charms_ops_mandatory(self, charm):
660 """672 """
661 Check if a mandatory ops charms is present in the model.673 Check if a mandatory ops charms is present in the model.
@@ -1228,9 +1240,8 @@ class Linter:
1228 def do_lint(self, parsed_yaml): # pragma: no cover1240 def do_lint(self, parsed_yaml): # pragma: no cover
1229 """Lint parsed YAML."""1241 """Lint parsed YAML."""
1230 # Handle Juju 2 vs Juju 11242 # Handle Juju 2 vs Juju 1
1231 applications = "applications"1243 applications = "applications" if "applications" in parsed_yaml else "services"
1232 if applications not in parsed_yaml:1244 input_file = input_handler(parsed_yaml, applications)
1233 applications = "services"
12341245
1235 if applications in parsed_yaml:1246 if applications in parsed_yaml:
12361247
@@ -1251,7 +1262,7 @@ class Linter:
1251 self.process_subordinates(parsed_yaml[applications][app], app)1262 self.process_subordinates(parsed_yaml[applications][app], app)
12521263
1253 self.check_subs(parsed_yaml["machines"])1264 self.check_subs(parsed_yaml["machines"])
1254 self.check_relations(parsed_yaml[applications])1265 self.check_relations(input_file)
1255 self.check_charms()1266 self.check_charms()
12561267
1257 if "relations" in parsed_yaml:1268 if "relations" in parsed_yaml:
diff --git a/jujulint/model_input.py b/jujulint/model_input.py
1258new file mode 1006441269new file mode 100644
index 0000000..a14551c
--- /dev/null
+++ b/jujulint/model_input.py
@@ -0,0 +1,331 @@
1#! /usr/bin/env python3
2from __future__ import annotations # type annotations are lazy-evaluated
3
4"""Treat input formats to lint."""
5
6from collections import defaultdict
7from dataclasses import dataclass, field
8from functools import partialmethod
9from logging import getLogger
10from typing import Any, Dict, List, Set, Tuple, Union
11
12from jujulint.util import extract_charm_name
13
14LOGGER = getLogger(__name__)
15
16
17@dataclass
18class BaseFile:
19 """BaseFile to define common properties and methods."""
20
21 applications_data: Dict
22 machines_data: Dict
23 charms: Set = field(default_factory=set)
24 machines: Set = field(default_factory=set)
25 app_to_charm: Dict = field(default_factory=dict)
26 charm_to_app: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
27 apps_to_machines: defaultdict[set] = field(default_factory=lambda: defaultdict(set))
28
29 def __post_init__(self):
30 """Dunder method to map file after instantiating."""
31 self.map_file()
32
33 @property
34 def applications(self) -> Set:
35 """Applications present in the file."""
36 return set(self.applications_data.keys())
37
38 @staticmethod
39 def split_relation(relation: List[List[str]]) -> Tuple[str, str]:
40 """Split relations into apps and endpoints.
41
42 :param relation: Relation having the following format:
43 [[<app_1>:<endpoint_1>], [<app_2>:<endpoint_2>]]
44 :type relation: List[List[str]]
45 :return: Relation and endpoint.
46 :rtype: Tuple[str, str]
47 """
48 return *relation[0].split(":"), *relation[1].split(":")
49
50 def map_file(self) -> None:
51 """Process the input file."""
52 for app in self.applications:
53 if "charm" in self.applications_data[app]:
54 charm_name = extract_charm_name(self.applications_data[app]["charm"])
55 self.charms.add(charm_name)
56 self.app_to_charm[app] = charm_name
57 self.charm_to_app[charm_name].add(app)
58 self.map_machines()
59 self.map_apps_to_machines()
60
61 def check_app_endpoint_existence(
62 self, app_endpoint: str, charm: str, endpoints_key: str
63 ) -> Tuple[str, str]:
64 """Check if app and endpoint exist on the object to lint.
65
66 :param app_endpoint: app and endpoint separated by ":" with the following format:
67 <app>:<endpoint>. When app is equal to "*", it's considered as ALL possible apps
68 :type app_endpoint: str
69 :param charm: charm to not check itself.
70 :type charm: str
71 :param endpoints_key: dictionary key to access endpoints.
72 :type endpoints_key: str
73 :return: application and endpoint
74 :rtype: Tuple[str, str]
75 """
76 app, endpoint = app_endpoint.split(":")
77 # app == "*" means all apps
78 # a charm from relation rule can have different app names.
79 if app != "*" and app != charm:
80 if app not in self.applications:
81 LOGGER.warning(f"{app} not found on applications.")
82 return "", ""
83
84 # NOTE(gabrielcocenza) it's not always that a bundle will contain all endpoints under "bindings".
85 # See LP#1949883 and LP#1990017
86 # juju-info is represented by "" on endpoint-bindings
87 if endpoint != "juju-info" and endpoint not in self.applications_data[
88 app
89 ].get(endpoints_key, {}):
90 LOGGER.warning(f"endpoint: {endpoint} not found on {app}")
91 return "", ""
92 return app, endpoint
93
94 def filter_by_app_and_endpoint(
95 self, charm: str, app: str, endpoint: str, endpoints_key: str
96 ) -> Set:
97 """Filter applications by the presence of an endpoint.
98
99 :param charm: Charm to not filter itself.
100 :type charm: str
101 :param app: Application to be filtered. When app is equal to "*", filters ALL apps
102 that have the endpoint passed.
103 :type app: str
104 :param endpoint: Endpoint of an application.
105 :type endpoint: str
106 :param endpoints_key: dictionary key to access endpoint.
107 :type endpoints_key: str
108 :return: Applications that matches with the endpoint passed.
109 :rtype: Set
110 """
111 # when app == "*", filters all apps that have the endpoint passed.
112 if app == "*":
113 # remove all possible app names to not check itself.
114 apps_to_check = self.applications - self.charm_to_app[charm]
115 return {
116 app
117 for app in apps_to_check
118 if endpoint
119 in self.applications_data.get(app, {}).get(endpoints_key, {})
120 }
121 return (
122 set([app])
123 if endpoint in self.applications_data.get(app, {}).get(endpoints_key, {})
124 else set()
125 )
126
127 def map_machines(self):
128 """Map machines method to be implemented.
129
130 :raises NotImplementedError: Raise if not implemented on child classes.
131 """
132 raise NotImplementedError(f"{self.__class__.__name__} missing: map_machines")
133
134 def map_apps_to_machines(self):
135 """Map apps to machines method to be implemented.
136
137 :raises NotImplementedError: Raise if not implemented on child classes.
138 """
139 raise NotImplementedError(
140 f"{self.__class__.__name__} missing: map_apps_to_machines"
141 )
142
143 def filter_by_relation(self, apps: Set, endpoint: str) -> Set:
144 """Filter apps by relation to be implemented.
145
146 :raises NotImplementedError: Raise if not implemented on child classes.
147 """
148 raise NotImplementedError(
149 f"{self.__class__.__name__} missing: filter_by_relation"
150 )
151
152 def sorted_machines(self, machine: str):
153 """Sort machines.
154
155 :param machine: machine id.
156 :type machine: str
157 :raises NotImplementedError: Raise if not implemented on child classes.
158 """
159 raise NotImplementedError(f"{self.__class__.__name__} missing: sorted_machines")
160
161
162@dataclass
163class JujuStatusFile(BaseFile):
164 """Juju status file input representation."""
165
166 def map_machines(self) -> None:
167 """Map machines passed on the file."""
168 self.machines.update(self.machines_data.keys())
169 for machine in self.machines_data:
170 self.machines.update(self.machines_data[machine].get("containers", []))
171
172 def map_apps_to_machines(self) -> None:
173 """Map applications on machines."""
174 for app in self.applications_data:
175 units = self.applications_data[app].get("units", {})
176 for unit in units:
177 machine = units[unit].get("machine")
178 self.apps_to_machines[app].add(machine)
179 subordinates = units[unit].get("subordinates", {})
180 for sub in subordinates:
181 self.apps_to_machines[sub.split("/")[0]].add(machine)
182
183 @staticmethod
184 def sorted_machines(machine: str) -> Tuple[int, str, int]:
185 """Sort machines by number and/or containers.
186
187 :param machine: name of the machine
188 E.g of expected input: "1", "1/lxd/3"
189 :type machine: str
190 :return: tuple with sort keys
191 :rtype: Tuple[int, str, int]
192 """
193 # way to be sure that a machine have at least 3 key values to sort
194 key_1, key_2, key_3, *_ = machine.split("/") + ["", 0]
195 return int(key_1), key_2, int(key_3)
196
197 # overwrite parent method passing "endpoint-bindings" as endpoints_key
198 filter_by_app_and_endpoint = partialmethod(
199 BaseFile.filter_by_app_and_endpoint, endpoints_key="endpoint-bindings"
200 )
201 check_app_endpoint_existence = partialmethod(
202 BaseFile.check_app_endpoint_existence, endpoints_key="endpoint-bindings"
203 )
204
205 def filter_by_relation(self, apps: Set, endpoint: str) -> Set:
206 """Filter applications that relate with an endpoint.
207
208 :param apps: Applications to filter relations using the endpoint.
209 :type apps: Set
210 :param endpoint: endpoint of the applications.
211 :type endpoint: str
212 :return: Applications that has a relation with the apps using the endpoint.
213 :rtype: Set
214 """
215 apps_related = set()
216 for app in apps:
217 relations = self.applications_data.get(app, {}).get("relations", {})
218 apps_related.update(relations.get(endpoint, []))
219 return apps_related
220
221
222@dataclass
223class JujuBundleFile(BaseFile):
224 """Juju bundle file input representation."""
225
226 relations_data: List = field(default_factory=list)
227
228 def map_machines(self) -> None:
229 """Map machines passed on the file."""
230 self.machines.update(self.machines_data.keys())
231 for app in self.applications_data:
232 deploy_to_machines = self.applications_data[app].get("to", [])
233 # openstack bundles can have corner cases e.g: to: - designate-bind/0
234 # in this case the application is deployed where designate-bind/0 is located
235 # See https://launchpad.net/bugs/1965256
236 machines = [machine for machine in deploy_to_machines if "/" not in machine]
237 self.machines.update(machines)
238
239 def map_apps_to_machines(self) -> None:
240 """Map applications on machines."""
241 for app in self.applications_data:
242 machines = self.applications_data[app].get("to", [])
243 self.apps_to_machines[app].update(machines)
244 # NOTE(gabrielcocenza) subordinates won't have the 'to' field because
245 # they are deployed thru relations.
246 subordinates = {
247 sub for sub, machines in self.apps_to_machines.items() if machines == set()
248 }
249 for relation in self.relations_data:
250 app_1, endpoint_1, app_2, endpoint_2 = self.split_relation(relation)
251 # update with the machines of the application that the subordinate charm relate.
252 if app_1 in subordinates:
253 self.apps_to_machines[app_1].update(self.apps_to_machines[app_2])
254 elif app_2 in subordinates:
255 self.apps_to_machines[app_2].update(self.apps_to_machines[app_1])
256
257 @staticmethod
258 def sorted_machines(machine: str) -> Tuple[int, str]:
259 """Sort machines by number and/or containers.
260
261 :param machine: name of the machine
262 E.g of expected input: "1", "lxd:1"
263 :type machine: str
264 :return: tuple with sort keys
265 :rtype: Tuple[int, str]
266 """
267 # way to be sure that a machine have at least 2 key values to sort
268 key_1, key_2, *_ = machine.split(":") + [""]
269
270 if key_1.isdigit():
271 # not container machines comes first.
272 return int(key_1), "0"
273 # in a container from a bundle, the machine number it's in the end. E.g: lxd:1
274 else:
275 return int(key_2), key_1
276
277 # overwrite parent method passing "bindings" as endpoints_key
278 filter_by_app_and_endpoint = partialmethod(
279 BaseFile.filter_by_app_and_endpoint, endpoints_key="bindings"
280 )
281 check_app_endpoint_existence = partialmethod(
282 BaseFile.check_app_endpoint_existence, endpoints_key="bindings"
283 )
284
285 def filter_by_relation(self, apps: Set, endpoint: str) -> Set:
286 """Filter applications that relate with an endpoint.
287
288 :param apps: Applications to filter relations using the endpoint.
289 :type apps: Set
290 :param endpoint: endpoint of the applications.
291 :type endpoint: str
292 :return: Applications that has a relation with the apps using the endpoint.
293 :rtype: Set
294 """
295 apps_related = set()
296 for relation in self.relations_data:
297 for app in apps:
298 app_ep = f"{app}:{endpoint}"
299 app_1_ep_1, app_2_ep_2 = relation
300 if app_1_ep_1 == app_ep:
301 apps_related.add(app_2_ep_2.split(":")[0])
302 elif app_2_ep_2 == app_ep:
303 apps_related.add(app_1_ep_1.split(":")[0])
304 return apps_related
305
306
307def input_handler(
308 parsed_yaml: Dict[str, Any], applications_key: str
309) -> Union[JujuStatusFile, JujuBundleFile]:
310 """Input handler to set right methods and fields.
311
312 :param parsed_yaml: input file that came from juju status or bundle.
313 :type parsed_yaml: Dict[str, Any]
314 :param applications_key: key to access applications or services (Juju v1)
315 :type applications_key: str
316 :return: Data Class from juju status or bundle.
317 :rtype: Union[JujuStatus, JujuBundle]
318 """
319 # relations key field is present just on bundles
320 if applications_key in parsed_yaml:
321 if "relations" in parsed_yaml:
322 return JujuBundleFile(
323 applications_data=parsed_yaml[applications_key],
324 machines_data=parsed_yaml["machines"],
325 relations_data=parsed_yaml["relations"],
326 )
327 else:
328 return JujuStatusFile(
329 applications_data=parsed_yaml[applications_key],
330 machines_data=parsed_yaml["machines"],
331 )
diff --git a/jujulint/relations.py b/jujulint/relations.py
index b6a39d2..6b85a6e 100644
--- a/jujulint/relations.py
+++ b/jujulint/relations.py
@@ -1,7 +1,9 @@
1#!/usr/bin/python31#!/usr/bin/python3
2"""Checks relations between applications."""2"""Checks relations between applications."""
3from logging import getLogger3from logging import getLogger
4from typing import Any, Dict, List, Set, Tuple4from typing import Any, Dict, List, Set, Union
5
6from jujulint.model_input import JujuBundleFile, JujuStatusFile
57
6LOGGER = getLogger(__name__)8LOGGER = getLogger(__name__)
79
@@ -23,46 +25,47 @@ class RelationRule:
2325
24 def __init__(26 def __init__(
25 self,27 self,
26 charm_to_app: Set,28 input_file: Union[JujuBundleFile, JujuStatusFile],
27 applications: Dict[str, Any],
28 charm: str,29 charm: str,
29 relations: List[List[str]],30 relations: List[List[str]],
30 not_exist: List[List[str]],31 not_exist: List[List[str]],
31 exception: Set,32 exception: Set,
33 ubiquitous: bool,
32 ):34 ):
33 """Relation Rule object.35 """Relation Rule object.
3436
35 :param charm_to_app: A charm can have more than one application name37 :param input_file: mapped content of the input file.
36 e.g:(nrpe-host and nrpe-container). This parameters contains all38 :type input_file: Union[JujuBundleFile, JujuStatusFile]
37 applications name of a given charm.
38 :type charm_to_app: Set
39 :param applications: All applications of the file passed to lint.
40 :type applications: Dict[str, Any]
41 :param charm: Name of the charm to check the relations rules.39 :param charm: Name of the charm to check the relations rules.
42 :type charm: str40 :type charm: str
43 :param relations: Relations that should be checked of a given charm41 :param relations: Relations that should be checked of a given charm
44 having the following format:42 having the following format:
45 [[<application>:<application_endpoint>, <application>:<application_endpoint>]]43 [[<app_1>:<endpoint_1>], [<app_2>:<endpoint_2>]]
46 If <application> == "*" it will check the relation using the endpoint44 If <application> == "*" it will check the relation using the endpoint
47 on all applications form the file passed.45 on all applications form the file passed.
48 :type relations: List[List[str]]46 :type relations: List[List[str]]
49 :param not_exist: Relation that should not exist in the presence of the47 :param not_exist: Relation that should not exist in the presence of the
50 relations passed to be checked. This parameter has the following format:48 relations passed to be checked. This parameter has the following format:
51 [[<application>:<application_endpoint>, <application>:<application_endpoint>]]49 [[<app_1>:<endpoint_1>], [<app_2>:<endpoint_2>]]
52 :type not_exist: List[List[str]],50 :type not_exist: List[List[str]],
53 :param exception: Set of applications that the rule doesn't apply to.51 :param exception: Set of applications that the rule doesn't apply to.
54 :type exception: Set52 :type exception: Set
53 :param ubiquitous: Check if charm is present on all machines.
54 :type ubiquitous: bool
55 """55 """
56 self.charm_to_app = charm_to_app56 self.input_file = input_file
57 self.applications = applications
58 self.charm = charm57 self.charm = charm
59 self.relations = relations58 self.relations = relations
60 self.not_exist = not_exist59 self.not_exist = not_exist
61 self.exception = exception60 self.exception = exception
61 self.ubiquitous = ubiquitous
62 # remove all possible app names from the subordinate app to not check itself62 # remove all possible app names from the subordinate app to not check itself
63 self.apps_to_check = set(self.applications.keys()) - self.charm_to_app63 # self.apps_to_check = (
64 # self.input_file.applications - self.input_file.charm_to_app[self.charm]
65 # )
64 self.missing_relations = dict()66 self.missing_relations = dict()
65 self.not_exist_error = list()67 self.not_exist_error = list()
68 self.missing_machines = set()
6669
67 @property70 @property
68 def relations(self) -> List[List[str]]:71 def relations(self) -> List[List[str]]:
@@ -89,16 +92,26 @@ class RelationRule:
89 return92 return
90 for relation_rule in raw_relations_rules:93 for relation_rule in raw_relations_rules:
91 try:94 try:
92 app_0, endpoint_0 = self.check_app_endpoint_existence(relation_rule[0])95 app_0, endpoint_0 = self.input_file.check_app_endpoint_existence(
93 app_1, endpoint_1 = self.check_app_endpoint_existence(relation_rule[1])96 relation_rule[0], self.charm
97 )
98 app_1, endpoint_1 = self.input_file.check_app_endpoint_existence(
99 relation_rule[1], self.charm
100 )
94 if not all([app_0, endpoint_0, app_1, endpoint_1]):101 if not all([app_0, endpoint_0, app_1, endpoint_1]):
95 # means that app or endpoint was not found102 # means that app or endpoint was not found
96 return103 return
97 if app_0 == self.charm:104 if (
105 app_0 == self.charm
106 or app_0 in self.input_file.charm_to_app[self.charm]
107 ):
98 self.endpoint = endpoint_0108 self.endpoint = endpoint_0
99 app_to_check = app_1109 app_to_check = app_1
100 endpoint_to_check = endpoint_1110 endpoint_to_check = endpoint_1
101 elif app_1 == self.charm:111 elif (
112 app_1 == self.charm
113 or app_1 in self.input_file.charm_to_app[self.charm]
114 ):
102 self.endpoint = endpoint_1115 self.endpoint = endpoint_1
103 app_to_check = app_0116 app_to_check = app_0
104 endpoint_to_check = endpoint_0117 endpoint_to_check = endpoint_0
@@ -111,8 +124,10 @@ class RelationRule:
111 return124 return
112125
113 # check if all apps variations has the endpoint126 # check if all apps variations has the endpoint
114 for app in self.charm_to_app:127 for app in self.input_file.charm_to_app[self.charm]:
115 self.check_app_endpoint_existence(f"{app}:{self.endpoint}")128 self.input_file.check_app_endpoint_existence(
129 f"{app}:{self.endpoint}", self.charm
130 )
116 self._relations.append([app_to_check, endpoint_to_check])131 self._relations.append([app_to_check, endpoint_to_check])
117 except (IndexError, ValueError) as e:132 except (IndexError, ValueError) as e:
118 raise RelationError(f"Relations rules has an unexpected format: {e}")133 raise RelationError(f"Relations rules has an unexpected format: {e}")
@@ -127,71 +142,32 @@ class RelationRule:
127142
128 def check(self) -> None:143 def check(self) -> None:
129 """Apply the relations rules check."""144 """Apply the relations rules check."""
130 self.relation_exist_check()145 try:
131 self.relation_not_exist_check()146 self.missing_machines = self.ubiquitous_check()
132147 self.relation_exist_check()
133 def filter_by_app_and_endpoint(self, app: str, endpoint: str) -> Set:148 self.relation_not_exist_check()
134 """Filter applications by the presence of an endpoint.149 except NotImplementedError as e:
135150 LOGGER.debug(e)
136 :param app: Application to be filtered.
137 :type app: str
138 :param endpoint: Endpoint of a application.
139 :type endpoint: str
140 :return: Applications that matched with the endpoint passed.
141 :rtype: Set
142 """
143 # NOTE(gabrielcocenza) this function just works with fields from juju status.
144 # when app == "*", filters all apps that have the endpoint passed.
145 if app == "*":
146 return {
147 app
148 for app in self.apps_to_check
149 if endpoint
150 in self.applications.get(app, {}).get("endpoint-bindings", {})
151 }
152 return (
153 set([app])
154 if endpoint in self.applications.get(app, {}).get("endpoint-bindings", {})
155 else set()
156 )
157
158 def filter_by_relation(self, apps: Set, endpoint: str) -> Set:
159 """Filter applications that relate with an endpoint.
160
161 :param apps: Applications to filter relations using the endpoint.
162 :type apps: Set
163 :param endpoint: endpoint of the applications.
164 :type endpoint: str
165 :return: Applications that has a relation with the apps using the endpoint.
166 :rtype: Set
167 """
168 # NOTE(gabrielcocenza) this function just works with fields from juju status.
169 apps_related = set()
170 for app in apps:
171 relations = self.applications.get(app, {}).get("relations", {})
172 apps_related.update(relations.get(endpoint, []))
173 return apps_related
174151
175 def relation_exist_check(self) -> None:152 def relation_exist_check(self) -> None:
176 """Check if app(s) are relating with an endpoint."""153 """Check if app(s) are relating with an endpoint."""
177 for relation in self.relations:154 for relation in self.relations:
178 app_to_check, endpoint_to_check = relation155 app_to_check, endpoint_to_check = relation
179 # applications in the bundle that have the endpoint to relate156 # applications in the bundle that have the endpoint to relate
180 apps_with_endpoint_to_check = self.filter_by_app_and_endpoint(157 apps_with_endpoint_to_check = self.input_file.filter_by_app_and_endpoint(
158 self.charm,
181 app_to_check,159 app_to_check,
182 endpoint_to_check,160 endpoint_to_check,
183 )161 )
184 # applications that are relating using the endpoint from the relation rule162 # applications that are relating using the endpoint from the relation rule
185 apps_related_with_relation_rule = self.filter_by_relation(163 apps_related_with_relation_rule = self.input_file.filter_by_relation(
186 self.charm_to_app,164 self.input_file.charm_to_app[self.charm],
187 self.endpoint,165 self.endpoint,
188 )166 )
189 self.missing_relations[f"{self.charm}:{self.endpoint}"] = sorted(167 self.missing_relations[f"{self.charm}:{self.endpoint}"] = sorted(
190 list(168 apps_with_endpoint_to_check
191 apps_with_endpoint_to_check169 - apps_related_with_relation_rule
192 - apps_related_with_relation_rule170 - self.exception
193 - self.exception
194 )
195 )171 )
196172
197 def relation_not_exist_check(self) -> None:173 def relation_not_exist_check(self) -> None:
@@ -204,7 +180,9 @@ class RelationRule:
204 app_endpoint_splitted = []180 app_endpoint_splitted = []
205 try:181 try:
206 for app_endpoint in relation:182 for app_endpoint in relation:
207 app, endpoint = self.check_app_endpoint_existence(app_endpoint)183 app, endpoint = self.input_file.check_app_endpoint_existence(
184 app_endpoint, self.charm
185 )
208 app_endpoint_splitted.extend([app, endpoint])186 app_endpoint_splitted.extend([app, endpoint])
209 (187 (
210 app_to_check_0,188 app_to_check_0,
@@ -212,7 +190,7 @@ class RelationRule:
212 app_to_check_1,190 app_to_check_1,
213 _,191 _,
214 ) = app_endpoint_splitted192 ) = app_endpoint_splitted
215 relations_app_endpoint_0 = self.filter_by_relation(193 relations_app_endpoint_0 = self.input_file.filter_by_relation(
216 {app_to_check_0}, endpoint_to_check_0194 {app_to_check_0}, endpoint_to_check_0
217 )195 )
218 if app_to_check_1 in relations_app_endpoint_0:196 if app_to_check_1 in relations_app_endpoint_0:
@@ -221,32 +199,23 @@ class RelationRule:
221 except (IndexError, ValueError) as e:199 except (IndexError, ValueError) as e:
222 raise RelationError(f"Problem during check_relation_not_exist: {e}")200 raise RelationError(f"Problem during check_relation_not_exist: {e}")
223201
224 def check_app_endpoint_existence(self, app_endpoint: str) -> Tuple[str, str]:202 def ubiquitous_check(self) -> List[str]:
225 """Check if app and endpoint exist on the object to lint.203 """Check if charm from relation rule is present on all machines.
226204
227 :param app_endpoint: app and endpoint separated by ":" with the following format:205 :return: Sorted list of machines missing the charm. If is present on
228 <application>:<application_endpoint>206 all machines, returns an empty list.
229 :type app_endpoint: str207 :rtype: List[str]
230 :return: application and endpoint
231 :rtype: Tuple[str, str]
232 """208 """
233 app, endpoint = app_endpoint.split(":")209 if self.ubiquitous:
234 # app == "*" means all apps210 machines_with_charm = set()
235 # a charm from relation rule can have different app names.211 for app in self.input_file.charm_to_app[self.charm]:
236 if app != "*" and app != self.charm:212 machines_with_charm.update(self.input_file.apps_to_machines[app])
237 if app not in self.applications.keys():213
238 LOGGER.warning(f"{app} not found on applications to check relations")214 return sorted(
239 return "", ""215 self.input_file.machines - machines_with_charm,
240216 key=self.input_file.sorted_machines,
241 # juju-info is represented by "" on endpoint-bindings217 )
242 if endpoint != "juju-info" and endpoint not in self.applications[app].get(218 return []
243 "endpoint-bindings", {}
244 ):
245 LOGGER.warning(
246 f"{app} don't have the endpoint: {endpoint} to check relations"
247 )
248 return "", ""
249 return app, endpoint
250219
251220
252class RelationsRulesBootStrap:221class RelationsRulesBootStrap:
@@ -254,24 +223,18 @@ class RelationsRulesBootStrap:
254223
255 def __init__(224 def __init__(
256 self,225 self,
257 charm_to_app: Dict[str, Set],
258 relations_rules: List[Dict[str, Any]],226 relations_rules: List[Dict[str, Any]],
259 applications: Dict[str, Any],227 input_file: Union[JujuBundleFile, JujuStatusFile],
260 ):228 ):
261 """Relations rules bootStrap object.229 """Relations rules bootStrap object.
262230
263 :param charm_to_app: A charm can have more than one application name
264 e.g:(nrpe-host and nrpe-container). This parameters contains all
265 applications name of a given charm.
266 :type charm_to_app: Dict[str, Set]
267 :param relations_rules: Relations rules from the rule file.231 :param relations_rules: Relations rules from the rule file.
268 :type relations_rules: List[Dict[str, Any]]232 :type relations_rules: List[Dict[str, Any]]
269 :param applications: All applications of the file passed to lint.233 :param input_file: mapped content of the input file.
270 :type applications: Dict[str, Any]234 :type input_file: Union[JujuBundleFile, JujuStatusFile]
271 """235 """
272 self.charm_to_app = charm_to_app
273 self.relations_rules = relations_rules236 self.relations_rules = relations_rules
274 self.applications = applications237 self.input_file = input_file
275238
276 def check(self) -> List[RelationRule]:239 def check(self) -> List[RelationRule]:
277 """Check all RelationRule objects.240 """Check all RelationRule objects.
@@ -281,12 +244,12 @@ class RelationsRulesBootStrap:
281 """244 """
282 relations_rules = [245 relations_rules = [
283 RelationRule(246 RelationRule(
284 charm_to_app=self.charm_to_app.get(rule.get("charm", ""), set()),247 input_file=self.input_file,
285 applications=self.applications,
286 charm=rule.get("charm"),248 charm=rule.get("charm"),
287 relations=rule.get("check", [[]]),249 relations=rule.get("check", [[]]),
288 not_exist=rule.get("not-exist", [[]]),250 not_exist=rule.get("not-exist", [[]]),
289 exception=set(rule.get("exception", set())),251 exception=set(rule.get("exception", set())),
252 ubiquitous=rule.get("ubiquitous", False),
290 )253 )
291 for rule in self.relations_rules254 for rule in self.relations_rules
292 ]255 ]
diff --git a/setup.py b/setup.py
index 9314d60..9bd497c 100644
--- a/setup.py
+++ b/setup.py
@@ -41,7 +41,7 @@ setuptools.setup(
41 "Environment :: Plugins",41 "Environment :: Plugins",
42 "Intended Audience :: System Administrators",42 "Intended Audience :: System Administrators",
43 ],43 ],
44 python_requires=">=3.4",44 python_requires=">=3.7",
45 packages=["jujulint"],45 packages=["jujulint"],
46 entry_points={"console_scripts": ["juju-lint=jujulint.cli:main"]},46 entry_points={"console_scripts": ["juju-lint=jujulint.cli:main"]},
47 setup_requires=["setuptools_scm"],47 setup_requires=["setuptools_scm"],
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 00500ea..b49a4b2 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -16,12 +16,13 @@ from unittest.mock import MagicMock
16import mock16import mock
17import pytest17import pytest
1818
19from jujulint import cloud # noqa: E402
20from jujulint.model_input import JujuBundleFile, JujuStatusFile
21
19# bring in top level library to path22# bring in top level library to path
20test_path = os.path.dirname(os.path.abspath(__file__))23test_path = os.path.dirname(os.path.abspath(__file__))
21sys.path.insert(0, test_path + "/../")24sys.path.insert(0, test_path + "/../")
2225
23from jujulint import cloud # noqa: E402
24
2526
26@pytest.fixture27@pytest.fixture
27def mocked_pkg_resources(monkeypatch):28def mocked_pkg_resources(monkeypatch):
@@ -130,6 +131,7 @@ def juju_status():
130 "charm": "cs:ntp-47",131 "charm": "cs:ntp-47",
131 "charm-name": "ntp",132 "charm-name": "ntp",
132 "relations": {"juju-info": ["ubuntu"]},133 "relations": {"juju-info": ["ubuntu"]},
134 "subordinate-to": ["ubuntu"],
133 "endpoint-bindings": {135 "endpoint-bindings": {
134 "": "external-space",136 "": "external-space",
135 "certificates": "external-space",137 "certificates": "external-space",
@@ -240,82 +242,237 @@ def rules_files():
240242
241243
242@pytest.fixture244@pytest.fixture
243def juju_status_relation():245def input_files(parsed_yaml_status, parsed_yaml_bundle):
246 return {
247 "juju-status": JujuStatusFile(
248 applications_data=parsed_yaml_status["applications"],
249 machines_data=parsed_yaml_status["machines"],
250 ),
251 "juju-bundle": JujuBundleFile(
252 applications_data=parsed_yaml_bundle["applications"],
253 machines_data=parsed_yaml_bundle["machines"],
254 relations_data=parsed_yaml_bundle["relations"],
255 ),
256 }
257
258
259@pytest.fixture
260def parsed_yaml_status():
244 """Representation of juju status input to test relations checks."""261 """Representation of juju status input to test relations checks."""
245 return {262 return {
246 "nrpe-container": {263 "applications": {
247 "charm": "cs:nrpe-61",264 "nrpe-container": {
248 "charm-name": "nrpe",265 "charm": "cs:nrpe-61",
249 "relations": {266 "charm-name": "nrpe",
250 "monitors": ["nagios"],267 "relations": {
251 "nrpe-external-master": [268 "nrpe-external-master": [
252 "keystone",269 "keystone",
253 ],270 ],
271 },
272 "endpoint-bindings": {
273 "general-info": "",
274 "local-monitors": "",
275 "monitors": "oam-space",
276 "nrpe": "",
277 "nrpe-external-master": "",
278 },
279 "subordinate-to": ["keystone"],
254 },280 },
255 "endpoint-bindings": {281 "nrpe-host": {
256 "general-info": "",282 "charm": "cs:nrpe-67",
257 "local-monitors": "",283 "charm-name": "nrpe",
258 "monitors": "oam-space",284 "relations": {
259 "nrpe": "",285 "nrpe-external-master": [
260 "nrpe-external-master": "",286 "elasticsearch",
287 ],
288 "general-info": ["ubuntu"],
289 },
290 "endpoint-bindings": {
291 "general-info": "",
292 "local-monitors": "",
293 "monitors": "oam-space",
294 "nrpe": "",
295 "nrpe-external-master": "",
296 },
297 "subordinate-to": ["elasticsearch", "ubuntu"],
261 },298 },
262 },299 "ubuntu": {
263 "nrpe-host": {300 "application-status": {"current": "active"},
264 "charm": "cs:nrpe-67",301 "charm": "cs:ubuntu-18",
265 "charm-name": "nrpe",302 "charm-name": "ubuntu",
266 "relations": {303 "relations": {"juju-info": ["nrpe-host"]},
267 "monitors": ["nagios"],304 "endpoint-bindings": {
268 "nrpe-external-master": [305 "": "external-space",
269 "elasticsearch",306 "certificates": "external-space",
270 ],307 },
308 "units": {
309 "ubuntu/0": {
310 "machine": "1",
311 "workload-status": {"current": "active"},
312 "subordinates": {
313 "nrpe-host/0": {
314 "workload-status": {
315 "current": "active",
316 }
317 }
318 },
319 }
320 },
271 },321 },
272 "endpoint-bindings": {322 "keystone": {
273 "general-info": "",323 "charm": "cs:keystone-309",
274 "local-monitors": "",324 "charm-name": "keystone",
275 "monitors": "oam-space",325 "relations": {
276 "nrpe": "",326 "nrpe-external-master": ["nrpe-container"],
277 "nrpe-external-master": "",327 },
328 "endpoint-bindings": {
329 "": "oam-space",
330 "admin": "external-space",
331 "certificates": "oam-space",
332 "cluster": "oam-space",
333 "domain-backend": "oam-space",
334 "ha": "oam-space",
335 "identity-admin": "oam-space",
336 "identity-credentials": "oam-space",
337 "identity-notifications": "oam-space",
338 "identity-service": "oam-space",
339 "internal": "internal-space",
340 "keystone-fid-service-provider": "oam-space",
341 "keystone-middleware": "oam-space",
342 "nrpe-external-master": "oam-space",
343 "public": "external-space",
344 "shared-db": "internal-space",
345 "websso-trusted-dashboard": "oam-space",
346 },
347 "units": {
348 "keystone/0": {
349 "machine": "1/lxd/0",
350 "subordinates": {
351 "nrpe-container/0": {
352 "workload-status": {
353 "current": "active",
354 }
355 }
356 },
357 }
358 },
359 },
360 "elasticsearch": {
361 "charm": "cs:elasticsearch-39",
362 "charm-name": "elasticsearch",
363 "relations": {
364 "nrpe-external-master": ["nrpe-host"],
365 },
366 "endpoint-bindings": {
367 "": "oam-space",
368 "client": "oam-space",
369 "data": "oam-space",
370 "logs": "oam-space",
371 "nrpe-external-master": "oam-space",
372 "peer": "oam-space",
373 },
374 "units": {
375 "elasticsearch/0": {
376 "machine": "0",
377 "subordinates": {
378 "nrpe-host/0": {
379 "workload-status": {
380 "current": "active",
381 }
382 }
383 },
384 }
385 },
278 },386 },
279 },387 },
280 "keystone": {388 "machines": {
281 "charm": "cs:keystone-309",389 "0": {
282 "charm-name": "keystone",390 "series": "focal",
283 "relations": {
284 "nrpe-external-master": ["nrpe-container"],
285 },391 },
286 "endpoint-bindings": {392 "1": {
287 "": "oam-space",393 "series": "focal",
288 "admin": "external-space",394 "containers": {
289 "certificates": "oam-space",395 "1/lxd/0": {
290 "cluster": "oam-space",396 "series": "focal",
291 "domain-backend": "oam-space",397 }
292 "ha": "oam-space",398 },
293 "identity-admin": "oam-space",
294 "identity-credentials": "oam-space",
295 "identity-notifications": "oam-space",
296 "identity-service": "oam-space",
297 "internal": "internal-space",
298 "keystone-fid-service-provider": "oam-space",
299 "keystone-middleware": "oam-space",
300 "nrpe-external-master": "oam-space",
301 "public": "external-space",
302 "shared-db": "internal-space",
303 "websso-trusted-dashboard": "oam-space",
304 },399 },
305 },400 },
306 "elasticsearch": {401 }
307 "charm": "cs:elasticsearch-39",402
308 "charm-name": "elasticsearch",403
309 "relations": {404@pytest.fixture
310 "nrpe-external-master": ["nrpe-host"],405def parsed_yaml_bundle():
406 """Representation of juju bundle input to test relations checks."""
407 return {
408 "series": "focal",
409 "applications": {
410 "elasticsearch": {
411 "bindings": {
412 "nrpe-external-master": "internal-space",
413 },
414 "charm": "elasticsearch",
415 "channel": "stable",
416 "revision": 59,
417 "num_units": 1,
418 "to": ["0"],
419 "constraints": "arch=amd64 mem=4096",
311 },420 },
312 "endpoint-bindings": {421 "keystone": {
313 "": "oam-space",422 "bindings": {
314 "client": "oam-space",423 "nrpe-external-master": "internal-space",
315 "data": "oam-space",424 "public": "internal-space",
316 "logs": "oam-space",425 },
317 "nrpe-external-master": "oam-space",426 "charm": "keystone",
318 "peer": "oam-space",427 "channel": "stable",
428 "revision": 539,
429 "resources": {"policyd-override": 0},
430 "num_units": 1,
431 "to": ["lxd:1"],
432 "constraints": "arch=amd64",
433 },
434 "nrpe-container": {
435 "bindings": {
436 "nrpe-external-master": "internal-space",
437 "local-monitors": "",
438 },
439 "charm": "nrpe",
440 "channel": "stable",
441 "revision": 94,
442 },
443 "nrpe-host": {
444 "bindings": {
445 "nrpe-external-master": "internal-space",
446 "local-monitors": "",
447 },
448 "charm": "nrpe",
449 "channel": "stable",
450 "revision": 94,
451 },
452 "ubuntu": {
453 "bindings": {"": "internal-space", "certificates": "external-space"},
454 "charm": "ubuntu",
455 "channel": "stable",
456 "revision": 21,
457 "num_units": 1,
458 "to": ["1"],
459 "options": {"hostname": ""},
460 "constraints": "arch=amd64 mem=4096",
319 },461 },
320 },462 },
463 "machines": {
464 "0": {"constraints": "arch=amd64 mem=4096"},
465 "1": {"constraints": "arch=amd64 mem=4096"},
466 },
467 "relations": [
468 [
469 "nrpe-container:nrpe-external-master",
470 "keystone:nrpe-external-master",
471 ],
472 ["nrpe-host:general-info", "ubuntu:juju-info"],
473 [
474 "elasticsearch:nrpe-external-master",
475 "nrpe-host:nrpe-external-master",
476 ],
477 ],
321 }478 }
diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py
322new file mode 100644479new file mode 100644
index 0000000..682b664
--- /dev/null
+++ b/tests/unit/test_input.py
@@ -0,0 +1,233 @@
1from dataclasses import dataclass
2
3import pytest
4
5from jujulint import model_input
6
7
8@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
9def test_file_inputs(input_files, input_file_type):
10 """Test that files are mapping properties as expected."""
11 input_file = input_files[input_file_type]
12 expected_output = {
13 "applications": {
14 "elasticsearch",
15 "ubuntu",
16 "keystone",
17 "nrpe-container",
18 "nrpe-host",
19 },
20 "machines": {
21 "juju-status": {"0", "1", "1/lxd/0"},
22 "juju-bundle": {"0", "1", "lxd:1"},
23 },
24 "charms": {"elasticsearch", "ubuntu", "nrpe", "keystone"},
25 "app_to_charm": {
26 "elasticsearch": "elasticsearch",
27 "ubuntu": "ubuntu",
28 "keystone": "keystone",
29 "nrpe-host": "nrpe",
30 "nrpe-container": "nrpe",
31 },
32 "charm_to_app": {
33 "nrpe": {"nrpe-container", "nrpe-host"},
34 "ubuntu": {"ubuntu"},
35 "elasticsearch": {"elasticsearch"},
36 "keystone": {"keystone"},
37 },
38 "apps_to_machines": {
39 "juju-status": {
40 "nrpe-container": {"1/lxd/0"},
41 "nrpe-host": {"0", "1"},
42 "ubuntu": {"1"},
43 "elasticsearch": {"0"},
44 "keystone": {"1/lxd/0"},
45 },
46 "juju-bundle": {
47 "nrpe-container": {"lxd:1"},
48 "nrpe-host": {"0", "1"},
49 "ubuntu": {"1"},
50 "elasticsearch": {"0"},
51 "keystone": {"lxd:1"},
52 },
53 },
54 }
55 assert input_file.applications == expected_output["applications"]
56 assert input_file.machines == expected_output["machines"][input_file_type]
57 assert (
58 input_file.apps_to_machines
59 == expected_output["apps_to_machines"][input_file_type]
60 )
61 assert input_file.charms == expected_output["charms"]
62 assert input_file.app_to_charm == expected_output["app_to_charm"]
63 assert input_file.charm_to_app == expected_output["charm_to_app"]
64
65
66@pytest.mark.parametrize(
67 "app_endpoint, app_error, endpoint_error, input_file_type, expected_output",
68 [
69 # app doesn't exist
70 ("foo:juju-info", True, False, "juju-status", ("", "")),
71 ("foo:juju-info", True, False, "juju-bundle", ("", "")),
72 # endpoint doesn't exist
73 ("keystone:bar", False, True, "juju-status", ("", "")),
74 ("keystone:bar", False, True, "juju-bundle", ("", "")),
75 # app and endpoint exist
76 (
77 "keystone:nrpe-external-master",
78 False,
79 False,
80 "juju-status",
81 ("keystone", "nrpe-external-master"),
82 ),
83 (
84 "keystone:nrpe-external-master",
85 False,
86 False,
87 "juju-bundle",
88 ("keystone", "nrpe-external-master"),
89 ),
90 ],
91)
92def test_check_app_endpoint_existence(
93 app_endpoint,
94 app_error,
95 endpoint_error,
96 mocker,
97 input_files,
98 input_file_type,
99 expected_output,
100):
101 """Test the expected check_app_endpoint_existence method behavior."""
102 input_file = input_files[input_file_type]
103 logger_mock = mocker.patch.object(model_input, "LOGGER")
104 app, endpoint = app_endpoint.split(":")
105 expected_msg = ""
106 if app_error:
107 expected_msg = f"{app} not found on applications."
108 elif endpoint_error:
109 expected_msg = f"endpoint: {endpoint} not found on {app}"
110
111 assert (
112 input_file.check_app_endpoint_existence(app_endpoint, "nrpe") == expected_output
113 )
114 if expected_msg:
115 logger_mock.warning.assert_has_calls([mocker.call(expected_msg)])
116
117
118@pytest.mark.parametrize(
119 "input_file_type, charm, app, endpoint, expected_output",
120 [
121 (
122 "juju-status",
123 "nrpe",
124 "*",
125 "nrpe-external-master",
126 {"keystone", "elasticsearch"},
127 ), # all apps with nrpe-external-master
128 (
129 "juju-bundle",
130 "nrpe",
131 "*",
132 "nrpe-external-master",
133 {"keystone", "elasticsearch"},
134 ), # all apps with nrpe-external-master
135 (
136 "juju-status",
137 "nrpe",
138 "keystone",
139 "nrpe-external-master",
140 {"keystone"},
141 ), # check if keystone has nrpe-external-master
142 (
143 "juju-bundle",
144 "nrpe",
145 "keystone",
146 "nrpe-external-master",
147 {"keystone"},
148 ), # check if keystone has nrpe-external-master
149 (
150 "juju-status",
151 "nrpe",
152 "ubuntu",
153 "nrpe-external-master",
154 set(),
155 ), # check if ubuntu has nrpe-external-master
156 (
157 "juju-bundle",
158 "nrpe",
159 "ubuntu",
160 "nrpe-external-master",
161 set(),
162 ), # check if ubuntu has nrpe-external-master
163 ],
164)
165def test_filter_by_app_and_endpoint(
166 input_files, input_file_type, charm, app, endpoint, expected_output
167):
168 """Test filter_by_app_and_endpoint method behave as expected."""
169 input_file = input_files[input_file_type]
170 assert (
171 input_file.filter_by_app_and_endpoint(charm, app, endpoint) == expected_output
172 )
173
174
175@pytest.mark.parametrize(
176 "input_file_type, endpoint, expected_output",
177 [
178 ("juju-status", "nrpe-external-master", {"keystone", "elasticsearch"}),
179 ("juju-bundle", "nrpe-external-master", {"keystone", "elasticsearch"}),
180 ("juju-status", "general-info", {"ubuntu"}),
181 ("juju-bundle", "general-info", {"ubuntu"}),
182 ("juju-status", "monitors", set()),
183 ("juju-bundle", "monitors", set()),
184 ],
185)
186def test_filter_by_relation(input_file_type, endpoint, expected_output, input_files):
187 """Test filter_by_relation method behave as expected."""
188 input_file = input_files[input_file_type]
189 assert (
190 input_file.filter_by_relation(input_file.charm_to_app["nrpe"], endpoint)
191 == expected_output
192 )
193
194
195@pytest.mark.parametrize(
196 "parsed_yaml, expected_output",
197 [
198 ("parsed_yaml_status", model_input.JujuStatusFile),
199 ("parsed_yaml_bundle", model_input.JujuBundleFile),
200 ],
201)
202def test_input_handler(parsed_yaml, expected_output, request):
203 """Input handler return expected objects depending on the input."""
204 input_file = request.getfixturevalue(parsed_yaml)
205 assert isinstance(
206 model_input.input_handler(input_file, "applications"), expected_output
207 )
208
209
210def test_raise_not_implemented_methods(parsed_yaml_status):
211 # declare a new input class
212 @dataclass
213 class MyNewInput(model_input.BaseFile):
214 # overwrite parent method to map file
215 def __post_init__(self):
216 return 0
217
218 new_input = MyNewInput(
219 applications_data=parsed_yaml_status["applications"],
220 machines_data=parsed_yaml_status["machines"],
221 )
222
223 with pytest.raises(NotImplementedError):
224 new_input.map_machines()
225
226 with pytest.raises(NotImplementedError):
227 new_input.map_apps_to_machines()
228
229 with pytest.raises(NotImplementedError):
230 new_input.filter_by_relation({"nrpe"}, "nrpe-external-master")
231
232 with pytest.raises(NotImplementedError):
233 new_input.sorted_machines("0")
diff --git a/tests/unit/test_jujulint.py b/tests/unit/test_jujulint.py
index 136044b..264b15e 100644
--- a/tests/unit/test_jujulint.py
+++ b/tests/unit/test_jujulint.py
@@ -1233,22 +1233,35 @@ applications:
1233 """Test conversion of string values (e.g. 2M (Megabytes)) to integers."""1233 """Test conversion of string values (e.g. 2M (Megabytes)) to integers."""
1234 assert linter.atoi(input_str) == expected_int1234 assert linter.atoi(input_str) == expected_int
12351235
1236 def test_check_relations_no_rules(self, linter, juju_status, mocker):1236 @pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
1237 def test_check_relations_no_rules(
1238 self, linter, input_files, mocker, input_file_type
1239 ):
1237 """Warn message if rule file doesn't pass relations to check."""1240 """Warn message if rule file doesn't pass relations to check."""
1238 mock_log = mocker.patch("jujulint.lint.Linter._log_with_header")1241 mock_log = mocker.patch("jujulint.lint.Linter._log_with_header")
1239 linter.check_relations(juju_status["applications"])1242 linter.check_relations(input_files[input_file_type])
1240 mock_log.assert_called_with("No relation rules found. Skipping relation checks")1243 mock_log.assert_called_with("No relation rules found. Skipping relation checks")
12411244
1242 def test_check_relations(self, linter, juju_status, mocker):1245 @pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
1246 def test_check_relations(self, linter, input_files, mocker, input_file_type):
1243 """Ensure that check_relation pass."""1247 """Ensure that check_relation pass."""
1244 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")1248 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")
1245 linter.lint_rules["relations"] = [1249 linter.lint_rules["relations"] = [
1246 {"charm": "ntp", "check": [["ntp:juju-info", "ubuntu:juju-info"]]}1250 {"charm": "nrpe", "check": [["nrpe:juju-info", "ubuntu:juju-info"]]}
1247 ]1251 ]
1248 linter.check_relations(juju_status["applications"])1252 linter.check_relations(input_files[input_file_type])
1249 mock_handle_error.assert_not_called()1253 mock_handle_error.assert_not_called()
12501254
1251 def test_check_relations_exception_handling(self, linter, juju_status, mocker):1255 @pytest.mark.parametrize(
1256 "input_file_type",
1257 [
1258 "juju-status",
1259 "juju-bundle",
1260 ],
1261 )
1262 def test_check_relations_exception_handling(
1263 self, linter, juju_status, mocker, input_file_type, input_files
1264 ):
1252 """Ensure that handle error if relation rules are in wrong format."""1265 """Ensure that handle error if relation rules are in wrong format."""
1253 mock_log = mocker.patch("jujulint.lint.Linter._log_with_header")1266 mock_log = mocker.patch("jujulint.lint.Linter._log_with_header")
1254 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")1267 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")
@@ -1260,44 +1273,88 @@ applications:
1260 "values to unpack (expected 2, got 1)"1273 "values to unpack (expected 2, got 1)"
1261 )1274 )
1262 expected_exception = relations.RelationError(expected_msg)1275 expected_exception = relations.RelationError(expected_msg)
1263 linter.check_relations(juju_status["applications"])1276
1277 linter.check_relations(input_files[input_file_type])
1264 mock_handle_error.assert_not_called()1278 mock_handle_error.assert_not_called()
1265 mock_log.assert_has_calls(1279 mock_log.assert_has_calls(
1266 [mocker.call(expected_exception.message, level=logging.ERROR)]1280 [mocker.call(expected_exception.message, level=logging.ERROR)]
1267 )1281 )
12681282
1269 def test_check_relations_missing_relations(self, linter, juju_status, mocker):1283 @pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
1284 def test_check_relations_missing_relations(
1285 self, linter, juju_status, mocker, input_file_type, input_files
1286 ):
1270 """Ensure that check_relation handle missing relations."""1287 """Ensure that check_relation handle missing relations."""
1271 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")1288 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")
1272 # add a relation rule that doesn't happen in the model1289 # add a relation rule that doesn't happen in the model
1273 linter.lint_rules["relations"] = [1290 linter.lint_rules["relations"] = [
1274 {"charm": "ntp", "check": [["ntp:certificates", "ubuntu:certificates"]]}1291 {
1292 "charm": "nrpe",
1293 "check": [["nrpe-host:local-monitors", "ubuntu:certificates"]],
1294 }
1275 ]1295 ]
1276 linter.check_relations(juju_status["applications"])1296 linter.check_relations(input_files[input_file_type])
1277 mock_handle_error.assert_called_with(1297 mock_handle_error.assert_called_with(
1278 {1298 {
1279 "id": "missing-relations",1299 "id": "missing-relations",
1280 "tags": ["relation", "missing"],1300 "tags": ["relation", "missing"],
1281 "message": "Endpoint '{}' is missing relations with: {}".format(1301 "message": "Endpoint '{}' is missing relations with: {}".format(
1282 "ntp:certificates", ["ubuntu"]1302 "nrpe:local-monitors", ["ubuntu"]
1283 ),1303 ),
1284 }1304 }
1285 )1305 )
12861306
1287 def test_check_relations_exist(self, linter, juju_status, mocker):1307 @pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
1308 def test_check_relations_exist(self, linter, input_files, mocker, input_file_type):
1288 """Ensure that check_relation handle not exist error."""1309 """Ensure that check_relation handle not exist error."""
1289 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")1310 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")
1311 not_exist_relation = [
1312 "nrpe-host:nrpe-external-master",
1313 "elasticsearch:nrpe-external-master",
1314 ]
1290 # add a relation rule that happen in the model1315 # add a relation rule that happen in the model
1291 linter.lint_rules["relations"] = [1316 linter.lint_rules["relations"] = [
1292 {"charm": "ntp", "not-exist": [["ntp:juju-info", "ubuntu:juju-info"]]}1317 {"charm": "nrpe", "not-exist": [not_exist_relation]}
1293 ]1318 ]
1294 linter.check_relations(juju_status["applications"])1319 linter.check_relations(input_files[input_file_type])
1295 mock_handle_error.assert_called_with(1320 mock_handle_error.assert_called_with(
1296 {1321 {
1297 "id": "relation-exist",1322 "id": "relation-exist",
1298 "tags": ["relation", "exist"],1323 "tags": ["relation", "exist"],
1299 "message": "Relation(s) {} should not exist.".format(1324 "message": "Relation(s) {} should not exist.".format(
1300 ["ntp:juju-info", "ubuntu:juju-info"]1325 not_exist_relation
1326 ),
1327 }
1328 )
1329
1330 @pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
1331 def test_check_relations_missing_machine(
1332 self, linter, input_files, mocker, input_file_type
1333 ):
1334 """Ensure that check_relation handle missing machines when ubiquitous."""
1335 new_machines = {"3": {"series": "focal"}, "2": {"series": "bionic"}}
1336 input_file = input_files[input_file_type]
1337 # add two new machines
1338 input_file.machines_data.update(new_machines)
1339 # map file again
1340 input_file.map_file()
1341 mock_handle_error = mocker.patch("jujulint.lint.Linter.handle_error")
1342 linter.lint_rules["relations"] = [
1343 {
1344 "charm": "nrpe",
1345 "ubiquitous": True,
1346 }
1347 ]
1348
1349 expected_missing_machines = ["2", "3"]
1350 linter.check_relations(input_file)
1351 mock_handle_error.assert_called_with(
1352 {
1353 "id": "missing-machine",
1354 "tags": ["missing", "machine"],
1355 "message": "Charm '{}' missing on machines: {}".format(
1356 "nrpe",
1357 expected_missing_machines,
1301 ),1358 ),
1302 }1359 }
1303 )1360 )
diff --git a/tests/unit/test_relations.py b/tests/unit/test_relations.py
index 9ec5d72..f13d544 100644
--- a/tests/unit/test_relations.py
+++ b/tests/unit/test_relations.py
@@ -11,127 +11,191 @@ RELATIONS = [["*:nrpe-external-master", "nrpe:nrpe-external-master"]]
1111
1212
13@pytest.mark.parametrize(13@pytest.mark.parametrize(
14 "correct_relation",14 "correct_relation, input_file_type",
15 [15 [
16 RELATIONS,16 (RELATIONS, "juju-status"),
17 [17 (RELATIONS, "juju-bundle"),
18 ["nrpe:nrpe-external-master", "*:nrpe-external-master"]18 (
19 ], # inverting sequence doesn't change the endpoint19 [["nrpe:nrpe-external-master", "*:nrpe-external-master"]],
20 [20 "juju-status",
21 ["nrpe:nrpe-external-master", "keystone:nrpe-external-master"]21 ), # inverting sequence doesn't change the endpoint
22 ], # able to find specific app relation22 (
23 [["nrpe:nrpe-external-master", "*:nrpe-external-master"]],
24 "juju-bundle",
25 ), # inverting sequence doesn't change the endpoint
26 (
27 [["nrpe:nrpe-external-master", "keystone:nrpe-external-master"]],
28 "juju-status",
29 ), # able to find specific app relation
30 (
31 [["nrpe:nrpe-external-master", "keystone:nrpe-external-master"]],
32 "juju-bundle",
33 ), # able to find specific app relation
23 ],34 ],
24)35)
25def test_relation_rule_valid(correct_relation, juju_status_relation):36def test_relation_rule_valid(correct_relation, input_file_type, input_files):
26 """Missing rules have empty set for endpoints with expected relations."""37 """Missing rules have empty set for endpoints with expected relations."""
27 relation_rule = relations.RelationRule(38 relation_rule = relations.RelationRule(
28 charm_to_app=CHARM_TO_APP,39 input_file=input_files[input_file_type],
29 applications=juju_status_relation,
30 charm=CHARM,40 charm=CHARM,
31 relations=correct_relation,41 relations=correct_relation,
32 not_exist=[[]],42 not_exist=[[]],
33 exception=set(),43 exception=set(),
44 ubiquitous=True,
34 )45 )
35 relation_rule.check()46 relation_rule.check()
36 assert relation_rule.missing_relations == {"nrpe:nrpe-external-master": list()}47 assert relation_rule.missing_relations == {"nrpe:nrpe-external-master": list()}
37 assert relation_rule.not_exist_error == list()48 assert relation_rule.not_exist_error == list()
49 assert relation_rule.missing_machines == list()
38 assert relation_rule.__repr__() == "RelationRule(nrpe -> nrpe-external-master)"50 assert relation_rule.__repr__() == "RelationRule(nrpe -> nrpe-external-master)"
39 assert relation_rule.endpoint == "nrpe-external-master"51 assert relation_rule.endpoint == "nrpe-external-master"
4052
4153
42def test_relation_not_exist(juju_status_relation):54@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
55def test_relation_not_exist(input_file_type, input_files):
43 """Ensure that finds a relation that shouldn't happen."""56 """Ensure that finds a relation that shouldn't happen."""
44 juju_status_relation["foo-charm"] = {57 wrong_relation = ["keystone:foo-endpoint", "foo-charm:foo-endpoint"]
45 "charm": "cs:foo-charm-7",58 input_file = input_files[input_file_type]
46 "charm-name": "foo-charm",59 if input_file_type == "juju-status":
47 "relations": {60 input_file.applications_data["foo-charm"] = {
48 "foo-endpoint": ["keystone"],61 "charm": "cs:foo-charm-7",
49 },62 "charm-name": "foo-charm",
50 "endpoint-bindings": {63 "relations": {
51 "": "oam-space",64 "foo-endpoint": ["keystone"],
52 "foo-endpoint": "oam-space",65 },
53 },66 "endpoint-bindings": {
54 }67 "": "oam-space",
55 juju_status_relation["keystone"]["relations"]["foo-endpoint"] = ["foo-charm"]68 "foo-endpoint": "oam-space",
56 juju_status_relation["keystone"]["endpoint-bindings"]["foo-endpoint"] = "oam-space"69 },
70 }
71 input_file.applications_data["keystone"]["relations"]["foo-endpoint"] = [
72 "foo-charm"
73 ]
74 input_file.applications_data["keystone"]["endpoint-bindings"][
75 "foo-endpoint"
76 ] = "oam-space"
77
78 elif input_file_type == "juju-bundle":
79 input_file.applications_data["foo-charm"] = {
80 "charm": "cs:foo-charm-7",
81 "charm-name": "foo-charm",
82 "bindings": {
83 "": "oam-space",
84 "foo-endpoint": "oam-space",
85 },
86 }
87 input_file.applications_data["keystone"]["bindings"][
88 "foo-endpoint"
89 ] = "oam-space"
90 input_file.relations_data.append(wrong_relation)
91
57 relation_rule = relations.RelationRule(92 relation_rule = relations.RelationRule(
58 charm_to_app=CHARM_TO_APP,93 input_file=input_file,
59 applications=juju_status_relation,
60 charm=CHARM,94 charm=CHARM,
61 relations=RELATIONS,95 relations=RELATIONS,
62 not_exist=[["keystone:foo-endpoint", "foo-charm:foo-endpoint"]],96 not_exist=[wrong_relation],
63 exception=set(),97 exception=set(),
98 ubiquitous=True,
64 )99 )
65 relation_rule.check()100 relation_rule.check()
66 assert relation_rule.not_exist_error == [101 assert relation_rule.not_exist_error == [wrong_relation]
67 ["keystone:foo-endpoint", "foo-charm:foo-endpoint"]
68 ]
69102
70103
71def test_relation_not_exist_raise(juju_status_relation):104@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
105def test_relation_not_exist_raise(input_file_type, input_files):
72 """Test that raise exception when not_exist has wrong format."""106 """Test that raise exception when not_exist has wrong format."""
73 juju_status_relation["foo-charm"] = {107 input_file = input_files[input_file_type]
74 "charm": "cs:foo-charm-7",108
75 "charm-name": "foo-charm",109 if input_file_type == "juju-status":
76 "relations": {110 input_file.applications_data["foo-charm"] = {
77 "bar-endpoint": ["keystone"],111 "charm": "cs:foo-charm-7",
78 },112 "charm-name": "foo-charm",
79 "endpoint-bindings": {113 "relations": {
80 "": "oam-space",114 "bar-endpoint": ["keystone"],
81 "bar-endpoint": "oam-space",115 },
82 },116 "endpoint-bindings": {
83 }117 "": "oam-space",
118 "bar-endpoint": "oam-space",
119 },
120 }
121
122 elif input_file_type == "juju-bundle":
123 input_file.applications_data["foo-charm"] = {
124 "charm": "cs:foo-charm-7",
125 "charm-name": "foo-charm",
126 "bindings": {
127 "": "oam-space",
128 "bar-endpoint": "oam-space",
129 },
130 }
84131
85 with pytest.raises(relations.RelationError):132 with pytest.raises(relations.RelationError):
86 relation_rule = relations.RelationRule(133 relation_rule = relations.RelationRule(
87 charm_to_app=CHARM_TO_APP,134 input_file=input_file,
88 applications=juju_status_relation,
89 charm=CHARM,135 charm=CHARM,
90 relations=RELATIONS,136 relations=RELATIONS,
91 not_exist=[["keystone", "foo-charm:foo-endpoint"]],137 not_exist=[["keystone", "foo-charm:foo-endpoint"]],
92 exception=set(),138 exception=set(),
139 ubiquitous=True,
93 )140 )
94 relation_rule.check()141 relation_rule.check()
95142
96143
97@pytest.mark.parametrize(144@pytest.mark.parametrize(
98 "expected_missing, exception",145 "expected_missing, exception, input_file_type",
99 [146 [
100 ({"nrpe:nrpe-external-master": ["foo-charm"]}, set()),147 ({"nrpe:nrpe-external-master": ["foo-charm"]}, set(), "juju-status"),
101 ({"nrpe:nrpe-external-master": list()}, {"foo-charm"}),148 ({"nrpe:nrpe-external-master": list()}, {"foo-charm"}, "juju-bundle"),
102 ],149 ],
103)150)
104def test_missing_relation_and_exception(151def test_missing_relation_and_exception(
105 expected_missing, exception, juju_status_relation152 expected_missing, exception, input_files, input_file_type
106):153):
107 """Assert that exception is able to remove apps missing the relation."""154 """Assert that exception rule field is able to remove apps missing the relation."""
108 # add a charm in apps that has the endpoint nrpe-external-master,155 # add a charm in apps that has the endpoint nrpe-external-master,
109 # but it's not relating with nrpe.156 # but it's not relating with nrpe.
110 juju_status_relation["foo-charm"] = {157 input_file = input_files[input_file_type]
111 "charm": "cs:foo-charm-7",158 if input_file_type == "juju-status":
112 "charm-name": "foo-charm",159 input_file.applications_data["foo-charm"] = {
113 "relations": {160 "charm": "cs:foo-charm-7",
114 "foo-endpoint": ["bar-charm"],161 "charm-name": "foo-charm",
115 },162 "relations": {
116 "endpoint-bindings": {163 "foo-endpoint": ["bar-charm"],
117 "": "oam-space",164 },
118 "nrpe-external-master": "oam-space",165 "endpoint-bindings": {
119 },166 "": "oam-space",
120 }167 "nrpe-external-master": "oam-space",
168 },
169 }
170 elif input_file_type == "juju-bundle":
171 input_file.applications_data["foo-charm"] = {
172 "charm": "cs:foo-charm-7",
173 "charm-name": "foo-charm",
174 "bindings": {
175 "": "oam-space",
176 "nrpe-external-master": "oam-space",
177 },
178 }
179 input_file.relations_data.append(
180 ["foo-charm:nrpe-external-master", "nrpe-host:nrpe-external-master"]
181 )
182
121 relation_rule = relations.RelationRule(183 relation_rule = relations.RelationRule(
122 charm_to_app=CHARM_TO_APP,184 input_file=input_file,
123 applications=juju_status_relation,
124 charm=CHARM,185 charm=CHARM,
125 relations=RELATIONS,186 relations=RELATIONS,
126 not_exist=[[]],187 not_exist=[[]],
127 exception=exception,188 exception=exception,
189 ubiquitous=True,
128 )190 )
129 relation_rule.check()191 relation_rule.check()
130 assert relation_rule.missing_relations == expected_missing192 assert relation_rule.missing_relations == expected_missing
131193
132194
133def test_relation_rule_unknown_charm(mocker, juju_status_relation):195@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
196def test_relation_rule_unknown_charm(mocker, input_files, input_file_type):
134 """Empty relation for a unknown charm in rules and gives warning message."""197 """Empty relation for a unknown charm in rules and gives warning message."""
198 input_file = input_files[input_file_type]
135 charm = "foo_charm"199 charm = "foo_charm"
136 warning_msg = (200 warning_msg = (
137 "Relations rules has an unexpected format. "201 "Relations rules has an unexpected format. "
@@ -139,51 +203,175 @@ def test_relation_rule_unknown_charm(mocker, juju_status_relation):
139 )203 )
140 logger_mock = mocker.patch.object(relations, "LOGGER")204 logger_mock = mocker.patch.object(relations, "LOGGER")
141 relation_rule = relations.RelationRule(205 relation_rule = relations.RelationRule(
142 charm_to_app=CHARM_TO_APP,206 input_file=input_file,
143 applications=juju_status_relation,
144 charm="foo_charm",207 charm="foo_charm",
145 relations=[["*:public", "keystone:public"]],208 relations=[["*:public", "keystone:public"]],
146 not_exist=[[]],209 not_exist=[[]],
147 exception=set(),210 exception=set(),
211 ubiquitous=False,
148 )212 )
149 assert relation_rule.relations == []213 assert relation_rule.relations == []
150 logger_mock.warning.assert_has_calls([mocker.call(warning_msg)])214 logger_mock.warning.assert_has_calls([mocker.call(warning_msg)])
151215
152216
153@pytest.mark.parametrize(217@pytest.mark.parametrize(
154 "fake_relations, app_error, endpoint_error",218 "fake_relations, app_error, endpoint_error, input_file_type",
155 [219 [
156 ([["foo:juju-info", "bar:juju-info"]], True, False), # app doesn't exist220 (
157 ([["keystone:bar", "nrpe-host:foo"]], False, True), # endpoint doesn't exist221 [["foo:juju-info", "bar:juju-info"]],
222 True,
223 False,
224 "juju-status",
225 ), # app doesn't exist
226 (
227 [["foo:juju-info", "bar:juju-info"]],
228 True,
229 False,
230 "juju-bundle",
231 ), # app doesn't exist
232 (
233 [["keystone:bar", "nrpe-host:foo"]],
234 False,
235 True,
236 "juju-status",
237 ), # endpoint doesn't exist
238 (
239 [["keystone:bar", "nrpe-host:foo"]],
240 False,
241 True,
242 "juju-bundle",
243 ), # endpoint doesn't exist
158 ],244 ],
159)245)
160def test_relation_rule_unknown_app_endpoint(246def test_relation_rule_unknown_app_endpoint(
161 fake_relations, app_error, endpoint_error, mocker, juju_status_relation247 fake_relations, app_error, endpoint_error, input_files, input_file_type
162):248):
163 """Ensure warning message and empty relations if app or endpoint is unknown."""249 """Ensure warning message and empty relations if app or endpoint is unknown."""
164 logger_mock = mocker.patch.object(relations, "LOGGER")250 input_file = input_files[input_file_type]
165 app, endpoint = fake_relations[0][0].split(":")
166 if app_error:
167 expected_msg = f"{app} not found on applications to check relations"
168 elif endpoint_error:
169 expected_msg = f"{app} don't have the endpoint: {endpoint} to check relations"
170251
171 relations_rule = relations.RelationRule(252 relations_rule = relations.RelationRule(
172 charm_to_app=CHARM_TO_APP,253 input_file=input_file,
173 applications=juju_status_relation,
174 charm=CHARM,254 charm=CHARM,
175 relations=fake_relations,255 relations=fake_relations,
176 not_exist=[[]],256 not_exist=[[]],
177 exception=set(),257 exception=set(),
258 ubiquitous=False,
178 )259 )
179 # assert that relations is empty260 # assert that relations is empty
180 assert relations_rule.relations == []261 assert relations_rule.relations == []
181 logger_mock.warning.assert_has_calls([mocker.call(expected_msg)])
182262
183263
184def test_relations_rules_bootstrap(juju_status_relation):264@pytest.mark.parametrize(
265 "machines, missing_machines, relations_to_check, input_file_type",
266 # adding new machines that nrpe is not relating
267 [
268 (
269 {"3": {"series": "focal"}, "2": {"series": "bionic"}},
270 ["2", "3"],
271 RELATIONS,
272 "juju-status",
273 ),
274 (
275 {"3": {"series": "focal"}, "2": {"series": "bionic"}},
276 ["2", "3"],
277 RELATIONS,
278 "juju-bundle",
279 ),
280 # empty relations is able to run ubiquitous check
281 (
282 {"3": {"series": "focal"}, "2": {"series": "bionic"}},
283 ["2", "3"],
284 [[]],
285 "juju-status",
286 ),
287 (
288 {"3": {"series": "focal"}, "2": {"series": "bionic"}},
289 ["2", "3"],
290 [[]],
291 "juju-bundle",
292 ),
293 (
294 {
295 "3": {
296 "series": "focal",
297 "containers": {
298 "3/lxd/0": {"series": "focal"},
299 "3/lxd/10": {"series": "focal"},
300 "3/lxd/1": {"series": "focal"},
301 "3/lxd/5": {"series": "focal"},
302 },
303 }
304 },
305 # result of missing machines is sorted
306 ["3", "3/lxd/0", "3/lxd/1", "3/lxd/5", "3/lxd/10"],
307 RELATIONS,
308 "juju-status",
309 ),
310 # bundles pass the machine to deploy the containers
311 (
312 {
313 "3": {
314 "series": "focal",
315 "containers": ["lxd:0", "lxd:3"],
316 }
317 },
318 # result of missing machines is sorted
319 ["lxd:0", "3", "lxd:3"],
320 RELATIONS,
321 "juju-bundle",
322 ),
323 ],
324)
325def test_ubiquitous_missing_machine(
326 input_files, machines, missing_machines, relations_to_check, input_file_type
327):
328 """Test that find missing machines for an ubiquitous charm."""
329 input_file = input_files[input_file_type]
330 if input_file_type == "juju-bundle":
331 for machine in machines:
332 containers = machines[machine].pop("containers", None)
333 if containers:
334 # pass new machines and containers to deploy keystone
335 input_file.applications_data["keystone"]["to"].extend(containers)
336 input_file.machines_data.update(machines)
337 # map machines again
338 input_file.map_file()
339 relation_rule = relations.RelationRule(
340 input_file=input_file,
341 charm=CHARM,
342 relations=relations_to_check,
343 not_exist=[[]],
344 exception=set(),
345 ubiquitous=True,
346 )
347 relation_rule.check()
348 assert relation_rule.missing_machines == missing_machines
349
350
351def test_relations_raise_not_implemented(input_files, mocker):
352 """Ensure that a new class that not implement mandatory methods raises error."""
353 logger_mock = mocker.patch.object(relations, "LOGGER")
354 mocker.patch(
355 "jujulint.relations.RelationRule.relation_exist_check",
356 side_effect=NotImplementedError(),
357 )
358 input_file = input_files["juju-status"]
359 relation_rule = relations.RelationRule(
360 input_file=input_file,
361 charm=CHARM,
362 relations=RELATIONS,
363 not_exist=[[]],
364 exception=set(),
365 ubiquitous=False,
366 )
367 relation_rule.check()
368 logger_mock.debug.assert_called_once()
369
370
371@pytest.mark.parametrize("input_file_type", ["juju-status", "juju-bundle"])
372def test_relations_rules_bootstrap(input_files, input_file_type):
185 """Test RelationsRulesBootStrap object."""373 """Test RelationsRulesBootStrap object."""
186 charm_to_app = {"nrpe": {"nrpe-host", "nrpe-container"}}374 input_file = input_files[input_file_type]
187 relations_rules = [375 relations_rules = [
188 {376 {
189 "charm": "nrpe",377 "charm": "nrpe",
@@ -197,9 +385,8 @@ def test_relations_rules_bootstrap(juju_status_relation):
197 },385 },
198 ]386 ]
199 relations_rules = relations.RelationsRulesBootStrap(387 relations_rules = relations.RelationsRulesBootStrap(
200 charm_to_app=charm_to_app,
201 relations_rules=relations_rules,388 relations_rules=relations_rules,
202 applications=juju_status_relation,389 input_file=input_file,
203 ).check()390 ).check()
204 assert len(relations_rules) == 2391 assert len(relations_rules) == 2
205 assert all(392 assert all(

Subscribers

People subscribed via source and target branches