Merge ~rgildein/juju-verify:bug/1917600 into ~canonical-solutions-engineering/juju-verify:master

Proposed by Robert Gildein
Status: Rejected
Rejected by: Robert Gildein
Proposed branch: ~rgildein/juju-verify:bug/1917600
Merge into: ~canonical-solutions-engineering/juju-verify:master
Diff against target: 398 lines (+252/-17)
5 files modified
juju_verify/utils/action.py (+9/-2)
juju_verify/utils/unit.py (+2/-1)
juju_verify/verifiers/ceph.py (+137/-3)
tests/unit/utils/test_action.py (+11/-3)
tests/unit/verifiers/test_ceph.py (+93/-8)
Reviewer Review Type Date Requested Status
🤖 prod-jenkaas-bootstack (community) continuous-integration Needs Fixing
Canonical Solutions Engineering Team Pending
Canonical Solutions Engineering Team Pending
Review via email: mp+399453@code.launchpad.net

Commit message

This MP add check of availability zones resources and it's used by CephOsd.verify_reboot and CephOsd.verify_shutdown.

Closses-bug: LP#1817600

To post a comment you must log in.
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
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.

~rgildein/juju-verify:bug/1917600 updated
3374f1d... by Robert Gildein

fix after rebase

091b6ed... by Robert Gildein

use new action "get-availability-zone" output

Unmerged commits

091b6ed... by Robert Gildein

use new action "get-availability-zone" output

3374f1d... by Robert Gildein

fix after rebase

58e9c34... by Robert Gildein

add unit test

3629ced... by Robert Gildein

finalized check_availability_zones_resources

ba99202... by Robert Gildein

add check_az_availability

ddadaeb... by Robert Gildein

add helper functions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/juju_verify/utils/action.py b/juju_verify/utils/action.py
2index bc31512..99c5f68 100644
3--- a/juju_verify/utils/action.py
4+++ b/juju_verify/utils/action.py
5@@ -15,15 +15,22 @@
6 # You should have received a copy of the GNU General Public License along with
7 # this program. If not, see https://www.gnu.org/licenses/.
8 """Helper function to manage Juju action."""
9+from typing import Dict
10+
11 from juju.action import Action
12
13
14+def results_from_action(action: Action) -> Dict:
15+ """Extract action's results."""
16+ return action.data.get('results', {})
17+
18+
19 def data_from_action(action: Action, key: str, default: str = '') -> str:
20 """Extract value from Action.data['results'] dictionary.
21
22 :param action: juju.Action instance
23 :param key: key to search for in action's results
24 :param default: default value to return if the 'key' is not found
25- :return: value from the action's result identified by 'key' or default
26+ :return: value from the action's results identified by 'key' or default
27 """
28- return action.data.get('results', {}).get(key, default)
29+ return results_from_action(action).get(key, default)
30diff --git a/juju_verify/utils/unit.py b/juju_verify/utils/unit.py
31index 0318cdb..1d4a4c2 100644
32--- a/juju_verify/utils/unit.py
33+++ b/juju_verify/utils/unit.py
34@@ -16,6 +16,7 @@
35 # this program. If not, see https://www.gnu.org/licenses/.
36 """Helper function to manage Juju unit."""
37 import asyncio
38+import os
39 import re
40 from typing import List, Dict
41
42@@ -59,7 +60,7 @@ def run_action_on_units(units: List[Unit], action: str,
43 unit))
44
45 if failed_actions_msg:
46- raise VerificationError('\n'.join(failed_actions_msg))
47+ raise VerificationError(os.linesep.join(failed_actions_msg))
48
49 return result_map
50
51diff --git a/juju_verify/verifiers/ceph.py b/juju_verify/verifiers/ceph.py
52index 28bca0a..cef7ead 100644
53--- a/juju_verify/verifiers/ceph.py
54+++ b/juju_verify/verifiers/ceph.py
55@@ -15,19 +15,70 @@
56 # You should have received a copy of the GNU General Public License along with
57 # this program. If not, see https://www.gnu.org/licenses/.
58 """ceph-osd verification."""
59+import json
60 import logging
61-from typing import Set
62+from dataclasses import dataclass
63+from typing import Set, Dict, List, Optional
64
65 from juju.unit import Unit
66
67 from juju_verify.utils.action import data_from_action
68-from juju_verify.utils.unit import verify_charm_unit, run_action_on_units
69+from juju_verify.utils.unit import (
70+ verify_charm_unit, run_action_on_units, parse_charm_name)
71 from juju_verify.verifiers.base import BaseVerifier
72 from juju_verify.verifiers.result import aggregate_results, Result
73
74 logger = logging.getLogger()
75
76
77+@dataclass
78+class AZ:
79+ """Availability zone."""
80+
81+ # pylint: disable=too-many-instance-attributes
82+ root: Optional[str] = None
83+ region: Optional[str] = None
84+ datacenter: Optional[str] = None
85+ room: Optional[str] = None
86+ pod: Optional[str] = None
87+ pdu: Optional[str] = None
88+ row: Optional[str] = None
89+ rack: Optional[str] = None
90+ chassis: Optional[str] = None
91+ host: Optional[str] = None
92+
93+ @property
94+ def crush_map_hierarchy(self) -> List[str]:
95+ """Get Ceph Crush Map hierarchy."""
96+ return [
97+ "root", # 10
98+ "region", # 9
99+ "datacenter", # 8
100+ "room", # 7
101+ "pod", # 6
102+ "pdu", # 5
103+ "row", # 4
104+ "rack", # 3
105+ "chassis", # 2
106+ "host", # 1
107+ "osd", # 0
108+ ]
109+
110+ def __str__(self) -> str:
111+ """Return string representation of AZ objects."""
112+ crush_map_types = []
113+ for crush_map_type in self.crush_map_hierarchy:
114+ value = getattr(self, crush_map_type, None)
115+ if value:
116+ crush_map_types.append(f"{crush_map_type}={value}")
117+
118+ return ",".join(crush_map_types)
119+
120+ def __hash__(self) -> int:
121+ """Return hash representation of AZ objects."""
122+ return hash(self.__str__())
123+
124+
125 class CephCommon(BaseVerifier): # pylint: disable=W0223
126 """Parent class for CephMon and CephOsd verifier."""
127
128@@ -61,6 +112,7 @@ class CephOsd(CephCommon):
129 """Implementation of verification checks for the ceph-osd charm."""
130
131 NAME = 'ceph-osd'
132+ _azs = None
133
134 def get_ceph_mon_units(self) -> Set[Unit]:
135 """Get Ceph-mon units related to verified units.
136@@ -87,9 +139,91 @@ class CephOsd(CephCommon):
137
138 return ceph_mon_units
139
140+ @property
141+ def azs(self) -> Dict[str, AZ]:
142+ """Get information about availability zone for each ceph-osd units."""
143+ if self._azs is None:
144+ self._azs = self.get_availability_zones()
145+
146+ return self._azs
147+
148+ def get_availability_zones(self) -> Dict[str, AZ]:
149+ """Get information about availability zone for each ceph-osd units.
150+
151+ This function return dictionary contain unit.entity_id as key and AZ info
152+ as value. e.g. {"ceph-osd/0": AZ(per_host=False, rack="nova", row=None)}
153+ """
154+ units = [unit for unit in self.model.units.values()
155+ if parse_charm_name(unit.charm_url) == "ceph-osd"]
156+ # NOTE (rgildein): This part should be tested after the review
157+ # https://review.opendev.org/c/openstack/charm-ceph-osd/+/778159
158+ # is merged.
159+ action_map = run_action_on_units(units, "get-availability-zone")
160+ availability_zones = {}
161+ for entity_id, action in action_map.items():
162+ action_result = data_from_action(action, "availability-zone", "{}")
163+ unit_az = json.loads(action_result).get("unit")
164+ availability_zones[entity_id] = AZ(**unit_az)
165+
166+ return availability_zones
167+
168+ def group_availability_zones(self) -> Dict[AZ, Dict[str, List[Unit]]]:
169+ """Group availability zones related to self.units."""
170+ grouped_az = {}
171+ for unit in self.units:
172+ unit_az = self.azs[unit.entity_id]
173+
174+ if unit_az not in grouped_az:
175+ grouped_az[unit_az] = {
176+ "units": [self.model.units.get(unit_id) for unit_id, az
177+ in self.azs.items() if az == unit_az],
178+ "verification_units": [unit],
179+ }
180+ else:
181+ grouped_az[unit_az]["verification_units"].append(unit)
182+
183+ return grouped_az
184+
185+ @staticmethod
186+ def get_number_of_replication_units(availability_zone: AZ) -> int:
187+ """Get number of replication units for availability zone."""
188+ if availability_zone.rack is None and availability_zone.row is None:
189+ return 1
190+
191+ # NOTE (rgildein): This is part of LP#1917599 and should return real number
192+ # of replication units in a rack or row.
193+ return 1
194+
195+ def check_availability_zones_resources(self) -> Result:
196+ """Check availability zones resources.
197+
198+ This function checks whether the units can be shutdown/reboot without
199+ interrupting operation in the availability zone.
200+ """
201+ result = Result(True)
202+ grouped_az = self.group_availability_zones()
203+
204+ for availability_zone, az_units in grouped_az.items():
205+ number_verification_units = len(az_units["verification_units"])
206+ number_replication_units = self.get_number_of_replication_units(
207+ availability_zone)
208+
209+ if number_replication_units - number_verification_units < 0:
210+ units = ",".join([unit.entity_id for unit
211+ in az_units["verification_units"]])
212+ result += Result(
213+ False,
214+ f"It's not safe to shutdown/reboot units {units} in the "
215+ f"availability zone {availability_zone}. [number_replication_units="
216+ f"{number_replication_units}]"
217+ )
218+
219+ return result
220+
221 def verify_reboot(self) -> Result:
222 """Verify that it's safe to reboot selected ceph-osd units."""
223- return aggregate_results(self.check_cluster_health(*self.get_ceph_mon_units()))
224+ return aggregate_results(self.check_cluster_health(*self.get_ceph_mon_units()),
225+ self.check_availability_zones_resources())
226
227 def verify_shutdown(self) -> Result:
228 """Verify that it's safe to shutdown selected ceph-osd units."""
229diff --git a/tests/unit/utils/test_action.py b/tests/unit/utils/test_action.py
230index 8ab8669..718566a 100644
231--- a/tests/unit/utils/test_action.py
232+++ b/tests/unit/utils/test_action.py
233@@ -20,7 +20,16 @@ from unittest.mock import PropertyMock
234 from juju.action import Action
235 from juju.model import Model
236
237-from juju_verify.utils.action import data_from_action
238+from juju_verify.utils.action import data_from_action, results_from_action
239+
240+
241+def test_results_from_action(mocker):
242+ """Test helper function parse results from Action.data."""
243+ data = {'results': "test_results"}
244+
245+ mocker.patch.object(Action, 'data', new_callable=PropertyMock(return_value=data))
246+
247+ assert results_from_action(Action('0', Model())) == "test_results"
248
249
250 def test_data_from_action(mocker):
251@@ -30,8 +39,7 @@ def test_data_from_action(mocker):
252 data = {'results': {host_key: host_value}}
253 default = 'default'
254
255- mocker.patch.object(Action, 'data',
256- new_callable=PropertyMock(return_value=data))
257+ mocker.patch.object(Action, 'data', new_callable=PropertyMock(return_value=data))
258 action = Action('0', Model())
259
260 output = data_from_action(action, host_key, default)
261diff --git a/tests/unit/verifiers/test_ceph.py b/tests/unit/verifiers/test_ceph.py
262index 728a4cb..2a8e23e 100644
263--- a/tests/unit/verifiers/test_ceph.py
264+++ b/tests/unit/verifiers/test_ceph.py
265@@ -15,14 +15,16 @@
266 # You should have received a copy of the GNU General Public License along with
267 # this program. If not, see https://www.gnu.org/licenses/.
268 """CephOsd verifier class test suite."""
269+import json
270 import os
271+from dataclasses import asdict
272 from unittest import mock
273 from unittest.mock import MagicMock
274
275 import pytest
276
277-from juju_verify.verifiers import CephOsd, Result
278-from juju_verify.verifiers.ceph import CephCommon
279+from juju_verify.verifiers.ceph import AZ, CephCommon, CephOsd
280+from juju_verify.verifiers.result import Result
281
282
283 @mock.patch("juju_verify.verifiers.ceph.run_action_on_units")
284@@ -88,25 +90,108 @@ def test_get_ceph_mon_units(model):
285 assert {ceph_mon_units[0]} == units
286
287
288+@pytest.mark.parametrize("az_info, ex_az", [
289+ ({"root": "default", "host": "test"}, "root=default,host=test"),
290+ ({"root": "default", "rack": "nova", "row": "row1", "host": "test"},
291+ "root=default,row=row1,rack=nova,host=test"),
292+])
293+def test_az(az_info, ex_az):
294+ """Test availability zone object."""
295+ availability_zone = AZ(**az_info)
296+ assert str(availability_zone) == ex_az
297+ assert hash(availability_zone) == hash(ex_az)
298+
299+
300+@mock.patch("juju_verify.verifiers.ceph.run_action_on_units")
301+def test_get_get_availability_zones(mock_run_action_on_units, model):
302+ """Test getting information about availability zones."""
303+ exp_az = AZ(root="default", rack="nova", host="test")
304+ mock_action = MagicMock()
305+ mock_action.data.get.side_effect = {
306+ "results": {"availability-zone": json.dumps({"unit": asdict(exp_az)})}}.get
307+ mock_run_action_on_units.return_value = {
308+ "ceph-osd/0": mock_action, "ceph-osd/1": mock_action
309+ }
310+
311+ azs = CephOsd([model.units.get("ceph-osd/0")]).azs
312+ mock_run_action_on_units.assert_called_once_with(
313+ [model.units.get("ceph-osd/0"), model.units.get("ceph-osd/1")],
314+ "get-availability-zone"
315+ )
316+ assert azs == {"ceph-osd/0": exp_az, "ceph-osd/1": exp_az}
317+
318+
319+def test_group_availability_zones(model):
320+ """Test grouping availability zones related to verifier.units."""
321+ exp_az = AZ(root="default", rack="nova", host="test")
322+ verifier = CephOsd([model.units.get("ceph-osd/0")])
323+ verifier._azs = {"ceph-osd/0": exp_az, "ceph-osd/1": exp_az}
324+
325+ related_az_group = verifier.group_availability_zones()
326+ assert related_az_group[exp_az]["units"] == [
327+ model.units.get("ceph-osd/0"), model.units.get("ceph-osd/1")]
328+ assert related_az_group[exp_az]["verification_units"] == [
329+ model.units.get("ceph-osd/0")]
330+
331+
332+@pytest.mark.parametrize("availability_zone, exp_number", [
333+ (AZ(root="default", host="test"), 1),
334+ (AZ(root="default", rack="nova", host="test"), 1),
335+ (AZ(root="default", rack="nova", row="row1", host="test"), 1),
336+])
337+def test_get_number_of_replication_units(availability_zone, exp_number, model):
338+ """Test getting number of replication units for availability zone."""
339+ verifier = CephOsd([model.units.get("ceph-osd/0")])
340+ assert verifier.get_number_of_replication_units(availability_zone) == exp_number
341+
342+
343+@mock.patch("juju_verify.verifiers.ceph.CephOsd.get_number_of_replication_units")
344+def test_check_availability_zones_resources(mock_get_number_of_replication_units, model):
345+ """Test check number of available resources in availability zone."""
346+ mock_get_number_of_replication_units.return_value = 1
347+ exp_az = AZ(root="default", host="test")
348+
349+ # verify one cep-osd units
350+ verifier = CephOsd([model.units.get("ceph-osd/0")])
351+ verifier._azs = {"ceph-osd/0": exp_az, "ceph-osd/1": exp_az}
352+
353+ result = verifier.check_availability_zones_resources()
354+ assert result == Result(True)
355+ mock_get_number_of_replication_units.assert_called_once_with(exp_az)
356+
357+ # verify two cep-osd units
358+ verifier = CephOsd([model.units.get("ceph-osd/0"), model.units.get("ceph-osd/1")])
359+ verifier._azs = {"ceph-osd/0": exp_az, "ceph-osd/1": exp_az, "ceph-osd/2": exp_az}
360+
361+ result = verifier.check_availability_zones_resources()
362+ assert result == Result(False, "It's not safe to shutdown/reboot units ceph-osd/0,"
363+ "ceph-osd/1 in the availability zone root=default,"
364+ "host=test. [number_replication_units=1]")
365+
366+
367 @mock.patch("juju_verify.verifiers.ceph.CephOsd.get_ceph_mon_units")
368 @mock.patch("juju_verify.verifiers.ceph.CephCommon.check_cluster_health")
369-def test_verify_reboot(mock_check_cluster_health, mock_get_ceph_mon_units, model):
370+@mock.patch("juju_verify.verifiers.ceph.CephOsd.check_availability_zones_resources")
371+def test_verify_reboot(mock_check_availability_zones_resources,
372+ mock_check_cluster_health, mock_get_ceph_mon_units, model):
373 """Test reboot verification on CephOsd."""
374 mock_get_ceph_mon_units.return_value = [model.units["ceph-mon/0"]]
375 mock_check_cluster_health.return_value = Result(True, "Ceph cluster is healthy")
376+ mock_check_availability_zones_resources.return_value = Result(True)
377
378- result = CephOsd([model.units["ceph-osd/0"]]).verify_reboot()
379-
380+ result = CephOsd([model.units.get("ceph-osd/0")]).verify_reboot()
381 assert result == Result(True, "Ceph cluster is healthy")
382
383
384 @mock.patch("juju_verify.verifiers.ceph.CephOsd.get_ceph_mon_units")
385 @mock.patch("juju_verify.verifiers.ceph.CephCommon.check_cluster_health")
386-def test_verify_shutdown(mock_check_cluster_health, mock_get_ceph_mon_units, model):
387+@mock.patch("juju_verify.verifiers.ceph.CephOsd.check_availability_zones_resources")
388+def test_verify_shutdown(mock_check_availability_zones_resources,
389+ mock_check_cluster_health, mock_get_ceph_mon_units, model):
390 """Test shutdown verification on CephOsd."""
391 mock_get_ceph_mon_units.return_value = [model.units["ceph-mon/0"]]
392 mock_check_cluster_health.return_value = Result(True, "Ceph cluster is healthy")
393+ mock_check_availability_zones_resources.return_value = Result(True)
394
395- result = CephOsd([model.units["ceph-osd/0"]]).verify_shutdown()
396-
397+ result = CephOsd([model.units.get("ceph-osd/0")]).verify_shutdown()
398 assert result == Result(True, "Ceph cluster is healthy")

Subscribers

People subscribed via source and target branches