From 282e23416f75e8fc4c0fc81c7ef68cedf6afe426 Mon Sep 17 00:00:00 2001 From: Xav Paice Date: Fri, 5 Mar 2021 17:01:14 +1300 Subject: [PATCH] Add get-quorum-status action Adds a new get-quorum-status action to return some distilled info from 'ceph quorum_status', primarily for verification of which mon units are online. Partial-Bug: #1917690 Change-Id: I608832d849ee3e4f5d150082c328b63c6ab43de7 --- actions.yaml | 10 ++++++++ actions/ceph_ops.py | 29 +++++++++++++++++++++- actions/get-quorum-status | 1 + actions/get_quorum_status.py | 37 ++++++++++++++++++++++++++++ unit_tests/test_actions_mon.py | 45 ++++++++++++++++++++++++++++++++-- 5 files changed, 119 insertions(+), 3 deletions(-) create mode 120000 actions/get-quorum-status create mode 100755 actions/get_quorum_status.py diff --git a/actions.yaml b/actions.yaml index e93645f6..c1ca254c 100644 --- a/actions.yaml +++ b/actions.yaml @@ -395,3 +395,13 @@ change-osd-weight: required: - osd - weight +get-quorum-status: + description: "Return lists of the known mons, and online mons, to determine if there is quorum." + params: + format: + type: string + default: text + enum: + - text + - json + description: Specify output format (text|json). diff --git a/actions/ceph_ops.py b/actions/ceph_ops.py index 875fe88d..5cc7b13a 100755 --- a/actions/ceph_ops.py +++ b/actions/ceph_ops.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json from subprocess import CalledProcessError, check_output import sys sys.path.append('hooks') -from charmhelpers.core.hookenv import action_get, action_fail +from charmhelpers.core.hookenv import ( + action_get, + action_fail, +) from charmhelpers.contrib.storage.linux.ceph import pool_set, \ set_pool_quota, snapshot_pool, remove_pool_snapshot @@ -143,3 +147,26 @@ def snapshot_ceph_pool(): snapshot_pool(service='ceph', pool_name=pool_name, snapshot_name=snapshot_name) + + +def get_quorum_status(format_type="text"): + """ + Return the output of 'ceph quorum_status'. + + On error, function_fail() is called with the exception info. + """ + ceph_output = check_output(['ceph', 'quorum_status'], + timeout=60).decode("utf-8") + ceph_output_json = json.loads(ceph_output) + + if format_type == "json": + return {"message": json.dumps(ceph_output_json)} + else: + return { + "election-epoch": ceph_output_json.get("election_epoch"), + "quorum-age": ceph_output_json.get("quorum_age"), + "quorum-leader-name": ceph_output_json.get("quorum_leader_name", + "unknown"), + "quorum-names": ", ".join(ceph_output_json.get("quorum_names", + [])), + } diff --git a/actions/get-quorum-status b/actions/get-quorum-status new file mode 120000 index 00000000..2ec9f01b --- /dev/null +++ b/actions/get-quorum-status @@ -0,0 +1 @@ +get_quorum_status.py \ No newline at end of file diff --git a/actions/get_quorum_status.py b/actions/get_quorum_status.py new file mode 100755 index 00000000..31f04890 --- /dev/null +++ b/actions/get_quorum_status.py @@ -0,0 +1,37 @@ +#!/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. + +"""Run action to collect Ceph quorum_status output.""" +import json +import sys + +from subprocess import CalledProcessError + +sys.path.append('hooks') + +from ceph_ops import get_quorum_status +from charmhelpers.core.hookenv import function_fail, function_get, function_set + +if __name__ == "__main__": + """Run action to collect Ceph quorum_status output.""" + try: + function_set(get_quorum_status(function_get("format"))) + except CalledProcessError as error: + function_fail("Failed to run ceph quorum_status, {}".format(error)) + except (json.decoder.JSONDecodeErro, KeyError) as error: + function_fail( + "Failed to parse ceph quorum_status output. {}".format(error) + ) diff --git a/unit_tests/test_actions_mon.py b/unit_tests/test_actions_mon.py index a4425aa1..fb749bf9 100644 --- a/unit_tests/test_actions_mon.py +++ b/unit_tests/test_actions_mon.py @@ -10,9 +10,9 @@ # 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. - -from mock import mock +import json import sys +from mock import mock from test_utils import CharmTestCase @@ -52,3 +52,44 @@ class OpsTestCase(CharmTestCase): actions.get_health() cmd = ['ceph', 'health'] self.check_output.assert_called_once_with(cmd) + + @mock.patch('socket.gethostname') + def test_get_quorum_status(self, mock_hostname): + mock_hostname.return_value = 'mockhost' + cmd_out = ( + '{"election_epoch":4,"quorum":[0,1,2],"quorum_names":["juju-18410c' + '-zaza-b7061340ed19-1","juju-18410c-zaza-b7061340ed19-0","juju-184' + '10c-zaza-b7061340ed19-2"],"quorum_leader_name":"juju-18410c-zaza-' + 'b7061340ed19-1","quorum_age":97785,"monmap":{"epoch":1,"fsid":"4f' + '9dd22a-1b71-11ec-a02a-fa163ee765d3","modified":"2021-09-22 06:51:' + '10.975225","created":"2021-09-22 06:51:10.975225","min_mon_releas' + 'e":14,"min_mon_release_name":"nautilus","features":{"persistent":' + '["kraken","luminous","mimic","osdmap-prune","nautilus"],"optional' + '":[]},"mons":[{"rank":0,"name":"juju-18410c-zaza-b7061340ed19-1",' + '"public_addrs":{"addrvec":[{"type":"v2","addr":"10.5.0.122:3300",' + '"nonce":0},{"type":"v1","addr":"10.5.0.122:6789","nonce":0}]},"ad' + 'dr":"10.5.0.122:6789/0","public_addr":"10.5.0.122:6789/0"},{"rank' + '":1,"name":"juju-18410c-zaza-b7061340ed19-0","public_addrs":{"add' + 'rvec":[{"type":"v2","addr":"10.5.2.239:3300","nonce":0},{"type":"' + 'v1","addr":"10.5.2.239:6789","nonce":0}]},"addr":"10.5.2.239:6789' + '/0","public_addr":"10.5.2.239:6789/0"},{"rank":2,"name":"juju-184' + '10c-zaza-b7061340ed19-2","public_addrs":{"addrvec":[{"type":"v2",' + '"addr":"10.5.3.201:3300","nonce":0},{"type":"v1","addr":"10.5.3.2' + '01:6789","nonce":0}]},"addr":"10.5.3.201:6789/0","public_addr":"1' + '0.5.3.201:6789/0"}]}}' + ) + self.check_output.return_value = cmd_out.encode() + + result = actions.get_quorum_status() + self.assertDictEqual(result, { + "election-epoch": 4, + "quorum-age": 97785, + "quorum-names": "juju-18410c-zaza-b7061340ed19-1, " + "juju-18410c-zaza-b7061340ed19-0, " + "juju-18410c-zaza-b7061340ed19-2", + "quorum-leader-name": "juju-18410c-zaza-b7061340ed19-1", + }) + + result = actions.get_quorum_status(format_type="json") + self.assertDictEqual(json.loads(result["message"]), + json.loads(cmd_out))