Merge "Add an action to provide information about AZ"

This commit is contained in:
Zuul 2021-09-07 12:11:00 +00:00 committed by Gerrit Code Review
commit e8a6a4f5c7
7 changed files with 277 additions and 1 deletions

View File

@ -240,6 +240,7 @@ is not deployed then see file `actions.yaml`.
* `add-disk`
* `blacklist-add-disk`
* `blacklist-remove-disk`
* `get-availibility-zone`
* `list-disks`
* `osd-in`
* `osd-out`

View File

@ -116,3 +116,18 @@ stop:
- osds
security-checklist:
description: Validate the running configuration against the OpenStack security guides checklist
get-availability-zone:
description: |
Obtain information about the availability zone, which will contain information about the CRUSH
structure. Specifically 'rack' and 'row'.
params:
format:
type: string
default: text
enum:
- text
- json
description: Specify output format (text|json).
show-all:
type: boolean
description: Option to view information for all units. Default is 'false'.

View File

@ -0,0 +1 @@
get_availability_zone.py

136
actions/get_availability_zone.py Executable file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
#
# Copyright 2021 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import sys
from tabulate import tabulate
sys.path.append("hooks")
sys.path.append("lib")
from charms_ceph.utils import get_osd_tree
from charmhelpers.core import hookenv
from utils import get_unit_hostname
CRUSH_MAP_HIERARCHY = [
"root", # 10
"region", # 9
"datacenter", # 8
"room", # 7
"pod", # 6
"pdu", # 5
"row", # 4
"rack", # 3
"chassis", # 2
"host", # 1
"osd", # 0
]
def _get_human_readable(availability_zones):
"""Get human readable table format.
:param availability_zones: information about the availability zone
:type availability_zones: Dict[str, Dict[str, str]]
:returns: formatted data as table
:rtype: str
"""
data = availability_zones.get(
"all-units", {get_unit_hostname(): availability_zones["unit"]}
)
data = [[unit, *crush_map.values()] for unit, crush_map in data.items()]
return tabulate(
data, tablefmt="grid", headers=["unit", *CRUSH_MAP_HIERARCHY]
)
def _get_crush_map(crush_location):
"""Get Crush Map hierarchy from CrushLocation.
:param crush_location: CrushLocation from function get_osd_tree
:type crush_location: charms_ceph.utils.CrushLocation
:returns: dictionary contains the Crush Map hierarchy, where
the keys are according to the defined types of the
Ceph Map Hierarchy
:rtype: Dict[str, str]
"""
return {
crush_map_type: getattr(crush_location, crush_map_type)
for crush_map_type in CRUSH_MAP_HIERARCHY
if getattr(crush_location, crush_map_type, None)
}
def get_availability_zones(show_all=False):
"""Get information about the availability zones.
Returns dictionary contains the unit as the current unit and other_units
(if the action was executed with the parameter show-all) that provide
information about other units.
:param show_all: define whether the result should contain AZ information
for all units
:type show_all: bool
:returns: {"unit": <current-unit-AZ>,
"all-units": {<unit-hostname>: <unit-AZ>}}
:rtype: Dict[str, Dict[str, str]]
"""
results = {"unit": {}, "all-units": {}}
osd_tree = get_osd_tree(service="osd-upgrade")
this_unit_host = get_unit_hostname()
for crush_location in osd_tree:
crush_map = _get_crush_map(crush_location)
if this_unit_host == crush_location.name:
results["unit"] = crush_map
results["all-units"][crush_location.name] = crush_map
if not show_all:
results.pop("all-units")
return results
def format_availability_zones(availability_zones, human_readable=True):
"""Format availability zones to action output format."""
if human_readable:
return _get_human_readable(availability_zones)
return json.dumps(availability_zones)
def main():
try:
show_all = hookenv.action_get("show-all")
human_readable = hookenv.action_get("format") == "text"
availability_zones = get_availability_zones(show_all)
if not availability_zones["unit"]:
hookenv.log(
"Availability zone information for current unit not found.",
hookenv.DEBUG
)
formatted_azs = format_availability_zones(availability_zones,
human_readable)
hookenv.action_set({"availability-zone": formatted_azs})
except Exception as error:
hookenv.action_fail("Action failed: {}".format(str(error)))
if __name__ == "__main__":
main()

View File

@ -2,7 +2,7 @@
# Wrapper to deal with newer Ubuntu versions that don't have py2 installed
# by default.
declare -a DEPS=('apt' 'pip' 'yaml')
declare -a DEPS=('apt' 'pip' 'yaml' 'tabulate')
check_and_install() {
pkg="${1}-${2}"

View File

@ -13,7 +13,11 @@
# limitations under the License.
import sys
from unittest.mock import MagicMock
sys.path.append('hooks')
sys.path.append('lib')
sys.path.append('actions')
sys.path.append('unit_tests')
sys.modules["tabulate"] = MagicMock()

View File

@ -0,0 +1,119 @@
# Copyright 2021 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from actions import get_availability_zone
from lib.charms_ceph.utils import CrushLocation
from test_utils import CharmTestCase
TABULATE_OUTPUT = """
+-------------+---------+-------------+
| unit | root | region |
+=============+=========+=============+
| juju-ceph-0 | default | juju-ceph-0 |
+-------------+---------+-------------+
| juju-ceph-1 | default | juju-ceph-1 |
+-------------+---------+-------------+
| juju-ceph-2 | default | juju-ceph-2 |
+-------------+---------+-------------+
"""
AVAILABILITY_ZONES = {
"unit": {"root": "default", "host": "juju-ceph-0"},
"all-units": {
"juju-ceph-0": {"root": "default", "host": "juju-ceph-0"},
"juju-ceph-1": {"root": "default", "host": "juju-ceph-1"},
"juju-ceph-2": {"root": "default", "host": "juju-ceph-2"}
}
}
class GetAvailabilityZoneActionTests(CharmTestCase):
def setUp(self):
super(GetAvailabilityZoneActionTests, self).setUp(
get_availability_zone,
["get_osd_tree", "get_unit_hostname", "tabulate"]
)
self.tabulate.return_value = TABULATE_OUTPUT
self.get_unit_hostname.return_value = "juju-ceph-0"
def test_get_human_readable(self):
"""Test formatting as human readable."""
table = get_availability_zone._get_human_readable(AVAILABILITY_ZONES)
self.assertTrue(table == TABULATE_OUTPUT)
def test_get_crush_map(self):
"""Test get Crush Map hierarchy from CrushLocation."""
crush_location = CrushLocation(
name="test", identifier="t1", host="test", rack=None, row=None,
datacenter=None, chassis=None, root="default")
crush_map = get_availability_zone._get_crush_map(crush_location)
self.assertDictEqual(crush_map, {"root": "default", "host": "test"})
crush_location = CrushLocation(
name="test", identifier="t1", host="test", rack="AZ",
row="customAZ", datacenter=None, chassis=None, root="default")
crush_map = get_availability_zone._get_crush_map(crush_location)
self.assertDictEqual(crush_map, {"root": "default", "row": "customAZ",
"rack": "AZ", "host": "test"})
def test_get_availability_zones(self):
"""Test function to get information about availability zones."""
self.get_unit_hostname.return_value = "test_1"
self.get_osd_tree.return_value = [
CrushLocation(name="test_1", identifier="t1", host="test_1",
rack="AZ1", row="AZ", datacenter=None,
chassis=None, root="default"),
CrushLocation(name="test_2", identifier="t2", host="test_2",
rack="AZ1", row="AZ", datacenter=None,
chassis=None, root="default"),
CrushLocation(name="test_3", identifier="t3", host="test_3",
rack="AZ2", row="AZ", datacenter=None,
chassis=None, root="default"),
CrushLocation(name="test_4", identifier="t4", host="test_4",
rack="AZ2", row="AZ", datacenter=None,
chassis=None, root="default"),
]
results = get_availability_zone.get_availability_zones()
self.assertDictEqual(results, {
"unit": dict(root="default", row="AZ", rack="AZ1", host="test_1")})
results = get_availability_zone.get_availability_zones(show_all=True)
self.assertDictEqual(results, {
"unit": dict(root="default", row="AZ", rack="AZ1", host="test_1"),
"all-units": {
"test_1": dict(root="default", row="AZ", rack="AZ1",
host="test_1"),
"test_2": dict(root="default", row="AZ", rack="AZ1",
host="test_2"),
"test_3": dict(root="default", row="AZ", rack="AZ2",
host="test_3"),
"test_4": dict(root="default", row="AZ", rack="AZ2",
host="test_4"),
}})
def test_format_availability_zones(self):
"""Test function to formatted availability zones."""
# human readable format
results_table = get_availability_zone.format_availability_zones(
AVAILABILITY_ZONES, True)
self.assertEqual(results_table, TABULATE_OUTPUT)
# json format
results_json = get_availability_zone.format_availability_zones(
AVAILABILITY_ZONES, False)
self.assertDictEqual(json.loads(results_json), AVAILABILITY_ZONES)