diff options
author | Rahman Syed <rahman.syed@gmail.com> | 2016-03-22 17:08:18 -0500 |
---|---|---|
committer | Rahman Syed <rahman.syed@gmail.com> | 2016-03-23 20:34:14 -0500 |
commit | ce50ad944490e6238597a72106f5be705db3aae8 (patch) | |
tree | b004924adfdbe4e511354d665f6f5c16c8a5ef62 /designateclient | |
parent | 575917105b51ab5ce37c61d1afee6301e3cdd9de (diff) | |
download | python-designateclient-ce50ad944490e6238597a72106f5be705db3aae8.tar.gz |
Implement zone export
Zone export commands (create, list, show, delete, showfile)
are missing from the python-designateclient.
This change includes implementation, as well as unit tests
and functionaltests.
Change-Id: I957946d739bceea1074e2fda2ce7f841143b0611
Partial-Bug: #1550532
Diffstat (limited to 'designateclient')
-rw-r--r-- | designateclient/functionaltests/client.py | 28 | ||||
-rw-r--r-- | designateclient/functionaltests/models.py | 17 | ||||
-rw-r--r-- | designateclient/functionaltests/v2/fixtures.py | 25 | ||||
-rw-r--r-- | designateclient/functionaltests/v2/test_zone_export.py | 94 | ||||
-rw-r--r-- | designateclient/tests/v2/test_zones.py | 65 | ||||
-rw-r--r-- | designateclient/v2/base.py | 4 | ||||
-rw-r--r-- | designateclient/v2/cli/zones.py | 143 | ||||
-rw-r--r-- | designateclient/v2/client.py | 5 | ||||
-rw-r--r-- | designateclient/v2/zones.py | 24 |
9 files changed, 369 insertions, 36 deletions
diff --git a/designateclient/functionaltests/client.py b/designateclient/functionaltests/client.py index 39dc970..370d48f 100644 --- a/designateclient/functionaltests/client.py +++ b/designateclient/functionaltests/client.py @@ -148,6 +148,31 @@ class ZoneTransferCommands(object): return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) +class ZoneExportCommands(object): + """A mixin for DesignateCLI to add zone export commands""" + + def zone_export_list(self, *args, **kwargs): + cmd = 'zone export list' + return self.parsed_cmd(cmd, ListModel, *args, **kwargs) + + def zone_export_create(self, zone_id, *args, **kwargs): + cmd = 'zone export create {0}'.format( + zone_id) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + def zone_export_show(self, zone_export_id, *args, **kwargs): + cmd = 'zone export show {0}'.format(zone_export_id) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + def zone_export_delete(self, zone_export_id, *args, **kwargs): + cmd = 'zone export delete {0}'.format(zone_export_id) + return self.parsed_cmd(cmd, *args, **kwargs) + + def zone_export_showfile(self, zone_export_id, *args, **kwargs): + cmd = 'zone export showfile {0}'.format(zone_export_id) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + class RecordsetCommands(object): def recordset_show(self, zone_id, id, *args, **kwargs): @@ -258,7 +283,8 @@ class BlacklistCommands(object): class DesignateCLI(base.CLIClient, ZoneCommands, ZoneTransferCommands, - RecordsetCommands, TLDCommands, BlacklistCommands): + ZoneExportCommands, RecordsetCommands, TLDCommands, + BlacklistCommands): # instantiate this once to minimize requests to keystone _CLIENTS = None diff --git a/designateclient/functionaltests/models.py b/designateclient/functionaltests/models.py index f298084..a6a307c 100644 --- a/designateclient/functionaltests/models.py +++ b/designateclient/functionaltests/models.py @@ -45,8 +45,23 @@ class FieldValueModel(Model): """ table = output_parser.table(out) + + # Because the output_parser handles Values with multiple lines + # in additional Field/Value pairs with Field name '', the following + # code is necessary to aggregate Values. + # + # The list of Field/Value pairs is in-order, so we can append Value + # continuation to the previously seen Field, with a newline separator. + value_lines = [] + prev_field = None for field, value in table['values']: - setattr(self, field, value) + if field == '': + value_lines.append(value) + setattr(self, prev_field, '\n'.join(value_lines)) + else: + setattr(self, field, value) + prev_field = field + value_lines = [value] class ListEntryModel(Model): diff --git a/designateclient/functionaltests/v2/fixtures.py b/designateclient/functionaltests/v2/fixtures.py index 54c3daa..2e61148 100644 --- a/designateclient/functionaltests/v2/fixtures.py +++ b/designateclient/functionaltests/v2/fixtures.py @@ -98,6 +98,31 @@ class TransferRequestFixture(BaseFixture): pass +class ExportFixture(BaseFixture): + """See DesignateCLI.zone_export_create for __init__ args""" + + def __init__(self, zone, user='default', *args, **kwargs): + super(ExportFixture, self).__init__(user, *args, **kwargs) + self.zone = zone + + def _setUp(self): + super(ExportFixture, self)._setUp() + self.zone_export = self.client.zone_export_create( + zone_id=self.zone.id, + *self.args, **self.kwargs + ) + self.addCleanup(self.cleanup_zone_export, self.client, + self.zone_export.id) + self.addCleanup(ZoneFixture.cleanup_zone, self.client, self.zone.id) + + @classmethod + def cleanup_zone_export(cls, client, zone_export_id): + try: + client.zone_export_delete(zone_export_id) + except CommandFailed: + pass + + class RecordsetFixture(BaseFixture): """See DesignateCLI.recordset_create for __init__ args""" diff --git a/designateclient/functionaltests/v2/test_zone_export.py b/designateclient/functionaltests/v2/test_zone_export.py new file mode 100644 index 0000000..fc10f85 --- /dev/null +++ b/designateclient/functionaltests/v2/test_zone_export.py @@ -0,0 +1,94 @@ +""" +Copyright 2016 Rackspace + +Author: Rahman Syed <rahman.syed@gmail.com> + +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. +""" +from designateclient.functionaltests.base import BaseDesignateTest +from designateclient.functionaltests.datagen import random_zone_name +from designateclient.functionaltests.v2.fixtures import ExportFixture +from designateclient.functionaltests.v2.fixtures import ZoneFixture + + +class TestZoneExport(BaseDesignateTest): + + def setUp(self): + super(TestZoneExport, self).setUp() + self.ensure_tld_exists('com') + fixture = self.useFixture(ZoneFixture( + name=random_zone_name(), + email='test@example.com', + )) + self.zone = fixture.zone + + def test_list_zone_exports(self): + zone_export = self.useFixture(ExportFixture( + zone=self.zone + )).zone_export + + zone_exports = self.clients.zone_export_list() + self.assertGreater(len(zone_exports), 0) + self.assertTrue(self._is_export_in_list(zone_export, zone_exports)) + + def test_create_and_show_zone_export(self): + zone_export = self.useFixture(ExportFixture( + zone=self.zone + )).zone_export + + fetched_export = self.clients.zone_export_show(zone_export.id) + + self.assertEqual(zone_export.created_at, fetched_export.created_at) + self.assertEqual(zone_export.id, fetched_export.id) + self.assertEqual(zone_export.message, fetched_export.message) + self.assertEqual(zone_export.project_id, fetched_export.project_id) + self.assertEqual(zone_export.zone_id, fetched_export.zone_id) + + def test_delete_zone_export(self): + zone_export = self.useFixture(ExportFixture( + zone=self.zone + )).zone_export + + zone_exports = self.clients.zone_export_list() + self.assertTrue(self._is_export_in_list(zone_export, zone_exports)) + + self.clients.zone_export_delete(zone_export.id) + + zone_exports = self.clients.zone_export_list() + self.assertFalse(self._is_export_in_list(zone_export, zone_exports)) + + def test_show_export_file(self): + zone_export = self.useFixture(ExportFixture( + zone=self.zone + )).zone_export + + fetched_export = self.clients.zone_export_showfile(zone_export.id) + + self.assertIn('$ORIGIN', fetched_export.data) + self.assertIn('$TTL', fetched_export.data) + self.assertIn('SOA', fetched_export.data) + self.assertIn('NS', fetched_export.data) + self.assertIn(self.zone.name, fetched_export.data) + + def _is_export_in_list(self, zone_export, zone_export_list): + """Determines if the given export exists in the given export list. + + Uses the zone export id for comparison. + + Because the zone export list command displays fewer fields than + the show command, an __eq__ method on the FieldValueModel class + is insufficient. + + """ + return any([export_record.id == zone_export.id + for export_record in zone_export_list]) diff --git a/designateclient/tests/v2/test_zones.py b/designateclient/tests/v2/test_zones.py index 8f7ac9d..673615e 100644 --- a/designateclient/tests/v2/test_zones.py +++ b/designateclient/tests/v2/test_zones.py @@ -13,6 +13,7 @@ # 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 time import uuid from designateclient.tests import v2 @@ -247,3 +248,67 @@ class TestZoneTransfers(v2.APIV2TestCase, v2.CrudMixin): response = self.client.zone_transfers.get_accept(accept_id) self.assertEqual(ref, response) + + +class TestZoneExports(v2.APIV2TestCase, v2.CrudMixin): + def new_ref(self, **kwargs): + ref = super(TestZoneExports, self).new_ref(**kwargs) + ref.setdefault("zone_id", uuid.uuid4().hex) + ref.setdefault("created_at", time.strftime("%c")) + ref.setdefault("updated_at", time.strftime("%c")) + ref.setdefault("status", 'PENDING') + ref.setdefault("version", '1') + return ref + + def test_create_export(self): + zone = uuid.uuid4().hex + ref = {} + + parts = ["zones", zone, "tasks", "export"] + self.stub_url('POST', parts=parts, json=ref) + + self.client.zone_exports.create(zone) + self.assertRequestBodyIs(json=ref) + + def test_get_export(self): + ref = self.new_ref() + + parts = ["zones", "tasks", "exports", ref["id"]] + self.stub_url('GET', parts=parts, json=ref) + self.stub_entity("GET", parts=parts, entity=ref, id=ref["id"]) + + response = self.client.zone_exports.get_export_record(ref["id"]) + self.assertEqual(ref, response) + + def test_list_exports(self): + items = [ + self.new_ref(), + self.new_ref() + ] + + parts = ["zones", "tasks", "exports"] + self.stub_url('GET', parts=parts, json={"exports": items}) + + listed = self.client.zone_exports.list() + self.assertList(items, listed["exports"]) + self.assertQueryStringIs("") + + def test_delete_export(self): + ref = self.new_ref() + + parts = ["zones", "tasks", "exports", ref["id"]] + self.stub_url('DELETE', parts=parts, json=ref) + self.stub_entity("DELETE", parts=parts, id=ref["id"]) + + self.client.zone_exports.delete(ref["id"]) + self.assertRequestBodyIs(None) + + def test_get_export_file(self): + ref = self.new_ref() + + parts = ["zones", "tasks", "exports", ref["id"], "export"] + self.stub_url('GET', parts=parts, json=ref) + self.stub_entity("GET", parts=parts, entity=ref, id=ref["id"]) + + response = self.client.zone_exports.get_export(ref["id"]) + self.assertEqual(ref, response) diff --git a/designateclient/v2/base.py b/designateclient/v2/base.py index e03cfe6..16d47c4 100644 --- a/designateclient/v2/base.py +++ b/designateclient/v2/base.py @@ -26,8 +26,8 @@ class DesignateList(list): class V2Controller(client.Controller): - def _get(self, url, response_key=None): - resp, body = self.client.session.get(url) + def _get(self, url, response_key=None, **kwargs): + resp, body = self.client.session.get(url, **kwargs) if response_key is not None: data = DesignateList() diff --git a/designateclient/v2/cli/zones.py b/designateclient/v2/cli/zones.py index 465c3ed..938a49a 100644 --- a/designateclient/v2/cli/zones.py +++ b/designateclient/v2/cli/zones.py @@ -33,6 +33,10 @@ def _format_zone(zone): zone['masters'] = ", ".join(zone['masters']) +def _format_zone_export_record(zone_export_record): + zone_export_record.pop('links', None) + + class ListZonesCommand(lister.Lister): """List zones""" @@ -205,38 +209,6 @@ class DeleteZoneCommand(command.Command): LOG.info('Zone %s was deleted', parsed_args.id) -class ExportZoneCommand(command.Command): - """Export a zone.""" - def get_parser(self, prog_name): - parser = super(ExportZoneCommand, self).get_parser(prog_name) - - parser.add_argument('id', help="Zone ID") - - return parser - - def take_action(self, parsed_args): - client = self.app.client_manager.dns - response, _ = client.zones.export(parsed_args.id) - print(response.content) - - -class ImportZoneCommand(command.Command): - """Import a zone""" - def get_parser(self, prog_name): - parser = super(ImportZoneCommand, self).get_parser(prog_name) - - parser.add_argument('--path', help="Path to zone file", required=True) - - return parser - - def take_action(self, parsed_args): - client = self.app.client_manager.dns - - with open(parsed_args.path) as contents: - client.zones.import_(contents) - LOG.info("Imported zone successfully") - - class AbandonZoneCommand(command.Command): """Abandon a zone""" def get_parser(self, prog_name): @@ -419,3 +391,110 @@ class ShowTransferAcceptCommand(show.ShowOne): data = client.zone_transfers.get_accept(parsed_args.id) return six.moves.zip(*sorted(six.iteritems(data))) + + +class ExportZoneCommand(show.ShowOne): + """Export a Zone""" + + def get_parser(self, prog_name): + parser = super(ExportZoneCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_id', help="Zone ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_exports.create(parsed_args.zone_id) + _format_zone_export_record(data) + + LOG.info('Zone Export %s was created', data['id']) + + return six.moves.zip(*sorted(six.iteritems(data))) + + +class ListZoneExportsCommand(lister.Lister): + """List Zone Exports""" + + columns = [ + 'id', + 'zone_id', + 'created_at', + 'status', + ] + + def get_parser(self, prog_name): + parser = super(ListZoneExportsCommand, self).get_parser( + prog_name) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_exports.list() + + cols = self.columns + return cols, (utils.get_item_properties(s, cols) + for s in data['exports']) + + +class ShowZoneExportCommand(show.ShowOne): + """Show a Zone Export""" + + def get_parser(self, prog_name): + parser = super(ShowZoneExportCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_export_id', help="Zone Export ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_exports.get_export_record( + parsed_args.zone_export_id) + _format_zone_export_record(data) + + return six.moves.zip(*sorted(six.iteritems(data))) + + +class DeleteZoneExportCommand(command.Command): + """Delete a Zone Export""" + + def get_parser(self, prog_name): + parser = super(DeleteZoneExportCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_export_id', help="Zone Export ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + client.zone_exports.delete(parsed_args.zone_export_id) + + LOG.info('Zone Export %s was deleted', parsed_args.zone_export_id) + + +class ShowZoneExportFileCommand(show.ShowOne): + """Show the zone file for the Zone Export""" + + def get_parser(self, prog_name): + parser = super(ShowZoneExportFileCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_export_id', help="Zone Export ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_exports.get_export(parsed_args.zone_export_id) + + return ['data'], [data] diff --git a/designateclient/v2/client.py b/designateclient/v2/client.py index daeb48e..2dc3f80 100644 --- a/designateclient/v2/client.py +++ b/designateclient/v2/client.py @@ -23,6 +23,8 @@ from designateclient.v2.recordsets import RecordSetController from designateclient.v2.reverse import FloatingIPController from designateclient.v2.tlds import TLDController from designateclient.v2.zones import ZoneController +from designateclient.v2.zones import ZoneExportsController +from designateclient.v2.zones import ZoneImportsController from designateclient.v2.zones import ZoneTransfersController from designateclient import version @@ -55,6 +57,7 @@ class DesignateAdapter(adapter.LegacyJsonAdapter): response_payload = response.json() except ValueError: response_payload = {} + body = response.text if response.status_code == 400: raise exceptions.BadRequest(**response_payload) @@ -97,3 +100,5 @@ class Client(object): self.tlds = TLDController(self) self.zones = ZoneController(self) self.zone_transfers = ZoneTransfersController(self) + self.zone_exports = ZoneExportsController(self) + self.zone_imports = ZoneImportsController(self) diff --git a/designateclient/v2/zones.py b/designateclient/v2/zones.py index 22631ab..8c200c8 100644 --- a/designateclient/v2/zones.py +++ b/designateclient/v2/zones.py @@ -124,3 +124,27 @@ class ZoneTransfersController(V2Controller): def get_accept(self, accept_id): url = '/zones/tasks/transfer_accepts/%s' % accept_id return self._get(url) + + +class ZoneExportsController(V2Controller): + def create(self, zone): + zone_id = v2_utils.resolve_by_name(self.client.zones.list, zone) + + return self._post('/zones/%s/tasks/export' % zone_id) + + def get_export_record(self, zone_export_id): + return self._get('/zones/tasks/exports/%s' % zone_export_id) + + def list(self): + return self._get('/zones/tasks/exports') + + def delete(self, zone_export_id): + return self._delete('/zones/tasks/exports/%s' % zone_export_id) + + def get_export(self, zone_export_id): + return self._get('/zones/tasks/exports/%s/export' % zone_export_id, + headers={'Accept': 'text/dns'}) + + +class ZoneImportsController(V2Controller): + pass |