From 4f586cb0603c086c590106b661ed886cffffd57a Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Tue, 14 Apr 2015 12:46:47 +0000 Subject: [PATCH] Allow to use domain names instead of ids Currently designate allows to identify a resource only by its id. This change allows to identify domains by name or id. This change defines the method find_resourceid_by_name_or_id which could be reused to identify other resources by name or id. Change-Id: I8e64cdbc5572623d05781d0c4e735ff0c429ea91 Closes-Bug: #1443858 --- designateclient/cli/base.py | 4 ++ designateclient/cli/domains.py | 20 +++++---- designateclient/cli/records.py | 31 +++++++++----- designateclient/exceptions.py | 4 ++ designateclient/tests/test_utils.py | 64 +++++++++++++++++++++++++++++ designateclient/utils.py | 22 ++++++++++ test-requirements.txt | 1 + 7 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 designateclient/tests/test_utils.py diff --git a/designateclient/cli/base.py b/designateclient/cli/base.py index 3fb3323..b01ae0d 100644 --- a/designateclient/cli/base.py +++ b/designateclient/cli/base.py @@ -82,6 +82,10 @@ class Command(CliffCommand): results = self.execute(parsed_args) return self.post_execute(results) + def find_resourceid_by_name_or_id(self, resource_plural, name_or_id): + resource_client = getattr(self.client, resource_plural) + return utils.find_resourceid_by_name_or_id(resource_client, name_or_id) + class ListCommand(Command, Lister): columns = None diff --git a/designateclient/cli/domains.py b/designateclient/cli/domains.py index fed6e3a..f848e1e 100644 --- a/designateclient/cli/domains.py +++ b/designateclient/cli/domains.py @@ -37,12 +37,13 @@ class GetDomainCommand(base.GetCommand): def get_parser(self, prog_name): parser = super(GetDomainCommand, self).get_parser(prog_name) - parser.add_argument('id', help="Domain ID") + parser.add_argument('id', help="Domain ID or Name") return parser def execute(self, parsed_args): - return self.client.domains.get(parsed_args.id) + id = self.find_resourceid_by_name_or_id('domains', parsed_args.id) + return self.client.domains.get(id) class CreateDomainCommand(base.CreateCommand): @@ -79,7 +80,7 @@ class UpdateDomainCommand(base.UpdateCommand): def get_parser(self, prog_name): parser = super(UpdateDomainCommand, self).get_parser(prog_name) - parser.add_argument('id', help="Domain ID") + parser.add_argument('id', help="Domain ID or Name") parser.add_argument('--name', help="Domain Name") parser.add_argument('--email', help="Domain Email") parser.add_argument('--ttl', type=int, help="Time To Live (Seconds)") @@ -91,7 +92,8 @@ class UpdateDomainCommand(base.UpdateCommand): def execute(self, parsed_args): # TODO(kiall): API needs updating.. this get is silly - domain = self.client.domains.get(parsed_args.id) + id = self.find_resourceid_by_name_or_id('domains', parsed_args.id) + domain = self.client.domains.get(id) if parsed_args.name: domain.name = parsed_args.name @@ -116,12 +118,13 @@ class DeleteDomainCommand(base.DeleteCommand): def get_parser(self, prog_name): parser = super(DeleteDomainCommand, self).get_parser(prog_name) - parser.add_argument('id', help="Domain ID") + parser.add_argument('id', help="Domain ID or Name") return parser def execute(self, parsed_args): - return self.client.domains.delete(parsed_args.id) + id = self.find_resourceid_by_name_or_id('domains', parsed_args.id) + return self.client.domains.delete(id) class ListDomainServersCommand(base.ListCommand): @@ -132,9 +135,10 @@ class ListDomainServersCommand(base.ListCommand): def get_parser(self, prog_name): parser = super(ListDomainServersCommand, self).get_parser(prog_name) - parser.add_argument('id', help="Domain ID") + parser.add_argument('id', help="Domain ID or Name") return parser def execute(self, parsed_args): - return self.client.domains.list_domain_servers(parsed_args.id) + id = self.find_resourceid_by_name_or_id('domains', parsed_args.id) + return self.client.domains.list_domain_servers(id) diff --git a/designateclient/cli/records.py b/designateclient/cli/records.py index ef8b6af..9e94137 100644 --- a/designateclient/cli/records.py +++ b/designateclient/cli/records.py @@ -30,12 +30,14 @@ class ListRecordsCommand(base.ListCommand): def get_parser(self, prog_name): parser = super(ListRecordsCommand, self).get_parser(prog_name) - parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('domain_id', help="Domain ID or Name") return parser def execute(self, parsed_args): - return self.client.records.list(parsed_args.domain_id) + domain_id = self.find_resourceid_by_name_or_id( + 'domains', parsed_args.domain_id) + return self.client.records.list(domain_id) class GetRecordCommand(base.GetCommand): @@ -44,13 +46,15 @@ class GetRecordCommand(base.GetCommand): def get_parser(self, prog_name): parser = super(GetRecordCommand, self).get_parser(prog_name) - parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('domain_id', help="Domain ID or Name") parser.add_argument('id', help="Record ID") return parser def execute(self, parsed_args): - return self.client.records.get(parsed_args.domain_id, parsed_args.id) + domain_id = self.find_resourceid_by_name_or_id( + 'domains', parsed_args.domain_id) + return self.client.records.get(domain_id, parsed_args.id) class CreateRecordCommand(base.CreateCommand): @@ -59,7 +63,7 @@ class CreateRecordCommand(base.CreateCommand): def get_parser(self, prog_name): parser = super(CreateRecordCommand, self).get_parser(prog_name) - parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('domain_id', help="Domain ID or Name") parser.add_argument('--name', help="Record Name", required=True) parser.add_argument('--type', help="Record Type", required=True) parser.add_argument('--data', help="Record Data", required=True) @@ -85,7 +89,9 @@ class CreateRecordCommand(base.CreateCommand): if parsed_args.description: record.description = parsed_args.description - return self.client.records.create(parsed_args.domain_id, record) + domain_id = self.find_resourceid_by_name_or_id( + 'domains', parsed_args.domain_id) + return self.client.records.create(domain_id, record) class UpdateRecordCommand(base.UpdateCommand): @@ -94,7 +100,7 @@ class UpdateRecordCommand(base.UpdateCommand): def get_parser(self, prog_name): parser = super(UpdateRecordCommand, self).get_parser(prog_name) - parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('domain_id', help="Domain ID or Name") parser.add_argument('id', help="Record ID") parser.add_argument('--name', help="Record Name") parser.add_argument('--type', help="Record Type") @@ -144,7 +150,9 @@ class UpdateRecordCommand(base.UpdateCommand): elif parsed_args.description: record.description = parsed_args.description - return self.client.records.update(parsed_args.domain_id, record) + domain_id = self.find_resourceid_by_name_or_id( + 'domains', parsed_args.domain_id) + return self.client.records.update(domain_id, record) class DeleteRecordCommand(base.DeleteCommand): @@ -153,11 +161,12 @@ class DeleteRecordCommand(base.DeleteCommand): def get_parser(self, prog_name): parser = super(DeleteRecordCommand, self).get_parser(prog_name) - parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('domain_id', help="Domain ID or Name") parser.add_argument('id', help="Record ID") return parser def execute(self, parsed_args): - return self.client.records.delete(parsed_args.domain_id, - parsed_args.id) + domain_id = self.find_resourceid_by_name_or_id( + 'domains', parsed_args.domain_id) + return self.client.records.delete(domain_id, parsed_args.id) diff --git a/designateclient/exceptions.py b/designateclient/exceptions.py index 871090c..7a857c1 100644 --- a/designateclient/exceptions.py +++ b/designateclient/exceptions.py @@ -23,6 +23,10 @@ class ResourceNotFound(Base): pass +class NoUniqueMatch(Base): + pass + + class RemoteError(Base): def __init__(self, message=None, code=None, type=None, errors=None, request_id=None): diff --git a/designateclient/tests/test_utils.py b/designateclient/tests/test_utils.py new file mode 100644 index 0000000..359d0e1 --- /dev/null +++ b/designateclient/tests/test_utils.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015 Thales Services SAS +# 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 uuid + +import mock + + +from designateclient import exceptions +from designateclient.tests import base +from designateclient import utils + + +LIST_MOCK_RESPONSE = [ + {'id': '13579bdf-0000-0000-abcd-000000000001', 'name': 'abcd'}, + {'id': '13579bdf-0000-0000-baba-000000000001', 'name': 'baba'}, + {'id': '13579bdf-0000-0000-baba-000000000002', 'name': 'baba'}, +] + + +class UtilsTestCase(base.TestCase): + + def _find_resourceid_by_name_or_id(self, name_or_id, by_name=False): + resource_client = mock.Mock() + resource_client.list.return_value = LIST_MOCK_RESPONSE + resourceid = utils.find_resourceid_by_name_or_id( + resource_client, name_or_id) + self.assertEqual(by_name, resource_client.list.called) + return resourceid + + def test_find_resourceid_with_hyphen_uuid(self): + expected = str(uuid.uuid4()) + observed = self._find_resourceid_by_name_or_id(expected) + self.assertEqual(expected, observed) + + def test_find_resourceid_with_nonhyphen_uuid(self): + expected = str(uuid.uuid4()) + fakeid = expected.replace('-', '') + observed = self._find_resourceid_by_name_or_id(fakeid) + self.assertEqual(expected, observed) + + def test_find_resourceid_with_unique_resource(self): + observed = self._find_resourceid_by_name_or_id('abcd', by_name=True) + self.assertEqual('13579bdf-0000-0000-abcd-000000000001', observed) + + def test_find_resourceid_with_nonexistent_resource(self): + self.assertRaises(exceptions.ResourceNotFound, + self._find_resourceid_by_name_or_id, + 'taz', by_name=True) + + def test_find_resourceid_with_multiple_resources(self): + self.assertRaises(exceptions.NoUniqueMatch, + self._find_resourceid_by_name_or_id, + 'baba', by_name=True) diff --git a/designateclient/utils.py b/designateclient/utils.py index e2414a7..dfba0a8 100644 --- a/designateclient/utils.py +++ b/designateclient/utils.py @@ -16,6 +16,7 @@ import json import os +import uuid from keystoneclient.auth.identity import generic @@ -141,3 +142,24 @@ def get_session(auth_url, endpoint, domain_id, domain_name, project_id, session.all_tenants = all_tenants return session + + +def find_resourceid_by_name_or_id(resource_client, name_or_id): + """Find resource id from its id or name.""" + try: + # Try to return a uuid + return str(uuid.UUID(name_or_id)) + except ValueError: + # Not a uuid => asume it is resource name + pass + + resources = resource_client.list() + candidate_ids = [r['id'] for r in resources if r.get('name') == name_or_id] + if not candidate_ids: + raise exceptions.ResourceNotFound( + 'Could not find resource with name "%s"' % name_or_id) + elif len(candidate_ids) > 1: + str_ids = ','.join(candidate_ids) + raise exceptions.NoUniqueMatch( + 'Multiple resources with name "%s": %s' % (name_or_id, str_ids)) + return candidate_ids[0] diff --git a/test-requirements.txt b/test-requirements.txt index 332f22e..bb81c3f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ # Hacking already pins down pep8, pyflakes and flake8 hacking>=0.9.2,<0.10 coverage>=3.6 +mock>=1.0 discover python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3