Add an action to provide information about AZ
The 'get-availability-zone' action will get information about an availability zone that will contain information about the CRUSH structure. Specifically 'rack' and 'row'. Closes-Bug: #1911006 Change-Id: I99ebbef5f23d6efe3c848b089c7f2b0d26ad0077
This commit is contained in:
parent
e5ac333a97
commit
d0d3d3edf5
@ -240,6 +240,7 @@ is not deployed then see file `actions.yaml`.
|
|||||||
* `add-disk`
|
* `add-disk`
|
||||||
* `blacklist-add-disk`
|
* `blacklist-add-disk`
|
||||||
* `blacklist-remove-disk`
|
* `blacklist-remove-disk`
|
||||||
|
* `get-availibility-zone`
|
||||||
* `list-disks`
|
* `list-disks`
|
||||||
* `osd-in`
|
* `osd-in`
|
||||||
* `osd-out`
|
* `osd-out`
|
||||||
|
15
actions.yaml
15
actions.yaml
@ -116,3 +116,18 @@ stop:
|
|||||||
- osds
|
- osds
|
||||||
security-checklist:
|
security-checklist:
|
||||||
description: Validate the running configuration against the OpenStack security guides 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
|
# Wrapper to deal with newer Ubuntu versions that don't have py2 installed
|
||||||
# by default.
|
# by default.
|
||||||
|
|
||||||
declare -a DEPS=('apt' 'pip' 'yaml')
|
declare -a DEPS=('apt' 'pip' 'yaml' 'tabulate')
|
||||||
|
|
||||||
check_and_install() {
|
check_and_install() {
|
||||||
pkg="${1}-${2}"
|
pkg="${1}-${2}"
|
||||||
|
@ -13,7 +13,11 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
sys.path.append('hooks')
|
sys.path.append('hooks')
|
||||||
sys.path.append('lib')
|
sys.path.append('lib')
|
||||||
sys.path.append('actions')
|
sys.path.append('actions')
|
||||||
sys.path.append('unit_tests')
|
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…
x
Reference in New Issue
Block a user