Merge "Add an action to provide information about AZ"
This commit is contained in:
commit
e8a6a4f5c7
@ -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`
|
||||
|
15
actions.yaml
15
actions.yaml
@ -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'.
|
||||
|
1
actions/get-availability-zone
Symbolic link
1
actions/get-availability-zone
Symbolic link
@ -0,0 +1 @@
|
||||
get_availability_zone.py
|
136
actions/get_availability_zone.py
Executable file
136
actions/get_availability_zone.py
Executable 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()
|
@ -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}"
|
||||
|
@ -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()
|
||||
|
119
unit_tests/test_actions_get_availability_zone.py
Normal file
119
unit_tests/test_actions_get_availability_zone.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user