diff --git a/actions.yaml b/actions.yaml index a33eaa63..5f7fcdf7 100644 --- a/actions.yaml +++ b/actions.yaml @@ -417,3 +417,25 @@ list-crush-rules: default: text description: "The output format, either json, yaml or text (default)" additionalProperties: false +get-or-create-user: + description: "Get or create a user and it's capabilities." + params: + username: + type: string + description: "User ID to get or create." + mon-caps: + type: string + default: allow rw + description: "Monitor capabilities include r, w, x access settings or profile {name}." + osd-caps: + type: string + default: allow rw + description: "OSD capabilities include r, w, x, class-read, class-write access settings or profile {name}." + required: [username] +delete-user: + description: "Delete a user." + params: + username: + type: string + description: "User ID to delete." + required: [username] \ No newline at end of file diff --git a/actions/delete-user b/actions/delete-user new file mode 120000 index 00000000..f55bc90f --- /dev/null +++ b/actions/delete-user @@ -0,0 +1 @@ +delete_user.py \ No newline at end of file diff --git a/actions/delete_user.py b/actions/delete_user.py new file mode 100755 index 00000000..93c6016c --- /dev/null +++ b/actions/delete_user.py @@ -0,0 +1,43 @@ +#!/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 sys + +sys.path.append('hooks') +from charmhelpers.core.hookenv import action_get, action_fail, action_set, log +from subprocess import CalledProcessError, check_output, STDOUT + + +def delete_user(): + username = action_get("username") + client = "client.{}".format(username) + try: + log(f'Attempting to delete credentials for entity {client}.') + output = check_output(['ceph', 'auth', 'del', client], + stderr=STDOUT).decode("utf-8") + return output + except CalledProcessError as e: + log(f'Failed to delete credentials for entity {client}.') + action_fail("User creation failed because of a failed process. " + "Ret Code: {} Message: {}".format(e.returncode, str(e))) + + +def main(): + action_set({"message": delete_user()}) + + +if __name__ == "__main__": + main() diff --git a/actions/get-or-create-user b/actions/get-or-create-user new file mode 120000 index 00000000..0060cdb0 --- /dev/null +++ b/actions/get-or-create-user @@ -0,0 +1 @@ +get_or_create_user.py \ No newline at end of file diff --git a/actions/get_or_create_user.py b/actions/get_or_create_user.py new file mode 100755 index 00000000..000855d7 --- /dev/null +++ b/actions/get_or_create_user.py @@ -0,0 +1,63 @@ +#!/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 sys +import json + +sys.path.append("hooks") +from charmhelpers.core.hookenv import action_get, action_fail, action_set, log +from subprocess import CalledProcessError, check_output + + +def get_or_create_user(): + username = action_get("username") + client = "client.{}".format(username) + try: + log(f'Attempting to retrieve existing credentials for entity {client}') + keyring = json.loads( + check_output(["ceph", "auth", "get", client, + "--format=json"]).decode("utf-8") + ) + log(f'Found existing credentials for entity {client}') + return json.dumps(keyring, indent=2) + except CalledProcessError: + log(f'Credentials for entity {client} not found') + pass + try: + log(f'Attempting to create new credentials for entity {client}') + mon_caps = action_get("mon-caps") + osd_caps = action_get("osd-caps") + log(f'with the following mon capabilities: {mon_caps},') + log(f'and osd capabilities: {osd_caps}.') + keyring = json.loads( + check_output(["ceph", "auth", "get-or-create", + client, "mon", mon_caps, "osd", osd_caps, + "--format=json"]).decode("utf-8") + ) + log(f'New credentials for entity {client} created') + return json.dumps(keyring, indent=2) + except CalledProcessError as e: + log(f'Failed to get or create credentials for entity {client}.') + action_fail("User creation failed because of a failed process. " + "Ret Code: {} Message: {}".format(e.returncode, str(e))) + + +def main(): + action_set({"message": get_or_create_user()}) + + +if __name__ == "__main__": + main() diff --git a/test-requirements.txt b/test-requirements.txt index e9401604..d515cae9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -44,7 +44,8 @@ git+https://github.com/openstack-charmers/zaza.git#egg=zaza git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack # Needed for charm-glance: -git+https://opendev.org/openstack/tempest.git#egg=tempest;python_version>='3.6' +git+https://opendev.org/openstack/tempest.git#egg=tempest;python_version>='3.8' +tempest<31.0.0;python_version<'3.8' tempest<24.0.0;python_version<'3.6' croniter # needed for charm-rabbitmq-server unit tests diff --git a/unit_tests/test_action_delete_user.py b/unit_tests/test_action_delete_user.py new file mode 100644 index 00000000..74c66201 --- /dev/null +++ b/unit_tests/test_action_delete_user.py @@ -0,0 +1,39 @@ +# 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. + +"""Tests for delete_user action.""" + +from actions import delete_user +from test_utils import CharmTestCase + + +class DeleteUserTestCase(CharmTestCase): + _stderr = b"""updated""" + + def setUp(self): + super(DeleteUserTestCase, self).setUp( + delete_user, ["check_output", "action_get", "action_fail", + "action_set", "log"]) + self.action_get.return_value = "sandbox" # username=sandbox + self.check_output.return_value = self._stderr + + def test_delete_user(self): + """Test getting status updated.""" + self.user = None + + def _action_set(message): + self.user = message["message"] + self.action_set.side_effect = _action_set + delete_user.main() + self.action_get.assert_called_once_with("username") + self.assertEqual(self.user, "updated") diff --git a/unit_tests/test_action_get_or_create_user.py b/unit_tests/test_action_get_or_create_user.py new file mode 100644 index 00000000..03127acb --- /dev/null +++ b/unit_tests/test_action_get_or_create_user.py @@ -0,0 +1,57 @@ +# 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. + +"""Tests for get_or_create_user action.""" + +import json + +from actions import get_or_create_user +from test_utils import CharmTestCase + + +class GetOrCreateUserTestCase(CharmTestCase): + _keyring = b""" + [ + { + "entity": "client.sandbox", + "key": "AQCnGXxiOkueGBAAsWX27MV8PNwuyMhPSzSCPg==", + "caps": { + "mon": "allow r", + "osd": "allow r" + } + } + ]""" + + def setUp(self): + super(GetOrCreateUserTestCase, self).setUp( + get_or_create_user, ["check_output", "action_get", "action_fail", + "action_set", "log"]) + self.action_get.return_value = "sandbox" # username=sandbox + self.check_output.return_value = self._keyring + + def test_get_or_create_user(self): + """Test getting resulting keyring.""" + self.user = None + + def _action_set(message): + self.user = json.loads(message["message"]) + self.action_set.side_effect = _action_set + get_or_create_user.main() + self.action_get.assert_called_once_with("username") + self.assertEqual(self.user[0]["entity"], "client.sandbox") + self.assertEqual( + self.user[0]["key"], + "AQCnGXxiOkueGBAAsWX27MV8PNwuyMhPSzSCPg==" + ) + self.assertEqual(self.user[0]["caps"]["mon"], "allow r") + self.assertEqual(self.user[0]["caps"]["osd"], "allow r")