diff --git a/actions.yaml b/actions.yaml index c1ca254c..a33eaa63 100644 --- a/actions.yaml +++ b/actions.yaml @@ -405,3 +405,15 @@ get-quorum-status: - text - json description: Specify output format (text|json). +list-crush-rules: + description: "List CEPH crush rules" + params: + format: + type: string + enum: + - json + - yaml + - text + default: text + description: "The output format, either json, yaml or text (default)" + additionalProperties: false diff --git a/actions/list-crush-rules b/actions/list-crush-rules new file mode 120000 index 00000000..30736b0d --- /dev/null +++ b/actions/list-crush-rules @@ -0,0 +1 @@ +list_crush_rules.py \ No newline at end of file diff --git a/actions/list_crush_rules.py b/actions/list_crush_rules.py new file mode 100755 index 00000000..a28fcc2b --- /dev/null +++ b/actions/list_crush_rules.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 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 os +import sys +import yaml +from subprocess import check_output, CalledProcessError + +_path = os.path.dirname(os.path.realpath(__file__)) +_hooks = os.path.abspath(os.path.join(_path, "../hooks")) + + +def _add_path(path): + if path not in sys.path: + sys.path.insert(1, path) + + +_add_path(_hooks) + + +from charmhelpers.core.hookenv import ( + ERROR, + log, + function_fail, + function_get, + function_set +) + + +def get_list_crush_rules(output_format="text"): + """Get list of Ceph crush rules. + + :param output_format: specify output format + :type output_format: str + :returns: text: list of tuple ( ) or + yaml: list of crush rules in yaml format + json: list of crush rules in json format + :rtype: str + """ + crush_rules = check_output(["ceph", "--id", "admin", "osd", "crush", + "rule", "dump", "-f", "json"]).decode("UTF-8") + crush_rules = json.loads(crush_rules) + + if output_format == "text": + return ",".join(["({}, {})".format(rule["rule_id"], rule["rule_name"]) + for rule in crush_rules]) + elif output_format == "yaml": + return yaml.dump(crush_rules) + else: + return json.dumps(crush_rules) + + +def main(): + try: + list_crush_rules = get_list_crush_rules(function_get("format")) + function_set({"message": list_crush_rules}) + except CalledProcessError as error: + log(error, ERROR) + function_fail("List crush rules failed with error: {}".format(error)) + + +if __name__ == "__main__": + main() diff --git a/unit_tests/test_action_change_osd_weight.py b/unit_tests/test_action_change_osd_weight.py index fbe26bd7..e0bff653 100644 --- a/unit_tests/test_action_change_osd_weight.py +++ b/unit_tests/test_action_change_osd_weight.py @@ -34,5 +34,4 @@ class ReweightTestCase(CharmTestCase): osd_num = 4 new_weight = 1.2 action.crush_reweight(osd_num, new_weight) - print(_reweight_osd.calls) _reweight_osd.assert_has_calls([mock.call("4", "1.2")]) diff --git a/unit_tests/test_action_list_crush_rules.py b/unit_tests/test_action_list_crush_rules.py new file mode 100644 index 00000000..87f52ecb --- /dev/null +++ b/unit_tests/test_action_list_crush_rules.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 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 yaml + +from actions import list_crush_rules +from test_utils import CharmTestCase + + +class ListCrushRulesTestCase(CharmTestCase): + ceph_osd_crush_rule_dump = b""" + [ + { + "rule_id": 0, + "rule_name": "replicated_rule", + "ruleset": 0, + "type": 1, + "min_size": 1, + "max_size": 10, + "steps": [ + { + "op": "take", + "item": -1, + "item_name": "default" + }, + { + "op": "chooseleaf_firstn", + "num": 0, + "type": "host" + }, + { + "op": "emit" + } + ] + }, + { + "rule_id": 1, + "rule_name": "test-host", + "ruleset": 1, + "type": 1, + "min_size": 1, + "max_size": 10, + "steps": [ + { + "op": "take", + "item": -1, + "item_name": "default" + }, + { + "op": "chooseleaf_firstn", + "num": 0, + "type": "host" + }, + { + "op": "emit" + } + ] + }, + { + "rule_id": 2, + "rule_name": "test-chassis", + "ruleset": 2, + "type": 1, + "min_size": 1, + "max_size": 10, + "steps": [ + { + "op": "take", + "item": -1, + "item_name": "default" + }, + { + "op": "chooseleaf_firstn", + "num": 0, + "type": "chassis" + }, + { + "op": "emit" + } + ] + }, + { + "rule_id": 3, + "rule_name": "test-rack-hdd", + "ruleset": 3, + "type": 1, + "min_size": 1, + "max_size": 10, + "steps": [ + { + "op": "take", + "item": -2, + "item_name": "default~hdd" + }, + { + "op": "chooseleaf_firstn", + "num": 0, + "type": "rack" + }, + { + "op": "emit" + } + ] + } + ] + """ + + def setUp(self): + super(ListCrushRulesTestCase, self).setUp( + list_crush_rules, ["check_output", "function_fail", "function_get", + "function_set"]) + self.function_get.return_value = "json" # format=json + self.check_output.return_value = self.ceph_osd_crush_rule_dump + + def test_getting_list_crush_rules_text_format(self): + """Test getting list of crush rules in text format.""" + self.function_get.return_value = "text" + list_crush_rules.main() + self.function_get.assert_called_once_with("format") + self.function_set.assert_called_once_with( + {"message": "(0, replicated_rule),(1, test-host)," + "(2, test-chassis),(3, test-rack-hdd)"}) + + def test_getting_list_crush_rules_json_format(self): + """Test getting list of crush rules in json format.""" + crush_rules = self.ceph_osd_crush_rule_dump.decode("UTF-8") + crush_rules = json.loads(crush_rules) + self.function_get.return_value = "json" + list_crush_rules.main() + self.function_get.assert_called_once_with("format") + self.function_set.assert_called_once_with( + {"message": json.dumps(crush_rules)}) + + def test_getting_list_crush_rules_yaml_format(self): + """Test getting list of crush rules in yaml format.""" + crush_rules = self.ceph_osd_crush_rule_dump.decode("UTF-8") + crush_rules = json.loads(crush_rules) + self.function_get.return_value = "yaml" + list_crush_rules.main() + self.function_get.assert_called_once_with("format") + self.function_set.assert_called_once_with( + {"message": yaml.dump(crush_rules)})