diff --git a/designateclient/client.py b/designateclient/client.py index 2c0e7b3..8b7305b 100644 --- a/designateclient/client.py +++ b/designateclient/client.py @@ -44,7 +44,10 @@ class Controller(object): } def _serialize(self, kwargs): - if 'data' in kwargs: + headers = kwargs.get('headers') + content_type = headers.get('Content-Type') if headers else None + + if 'data' in kwargs and content_type in {None, 'application/json'}: kwargs['data'] = json.dumps(kwargs['data']) def _post(self, url, response_key=None, **kwargs): diff --git a/designateclient/functionaltests/base.py b/designateclient/functionaltests/base.py index 5d6835c..faa0fcb 100644 --- a/designateclient/functionaltests/base.py +++ b/designateclient/functionaltests/base.py @@ -31,3 +31,16 @@ class BaseDesignateTest(base.ClientTestBase): self.clients.as_user('admin').tld_create(tld) except CommandFailed: pass + + def _is_entity_in_list(self, entity, entity_list): + """Determines if the given entity exists in the given list. + + Uses the id for comparison. + + Certain entities (e.g. zone import, export) cannot be made + comparable in a list of CLI output results, because the fields + in a list command can be different from those in a show command. + + """ + return any([entity_record.id == entity.id + for entity_record in entity_list]) diff --git a/designateclient/functionaltests/client.py b/designateclient/functionaltests/client.py index f6f9297..d68ad2f 100644 --- a/designateclient/functionaltests/client.py +++ b/designateclient/functionaltests/client.py @@ -175,6 +175,26 @@ class ZoneExportCommands(object): return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) +class ZoneImportCommands(object): + """A mixin for DesignateCLI to add zone import commands""" + + def zone_import_list(self, *args, **kwargs): + cmd = 'zone import list' + return self.parsed_cmd(cmd, ListModel, *args, **kwargs) + + def zone_import_create(self, zone_file_path, *args, **kwargs): + cmd = 'zone import create {0}'.format(zone_file_path) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + def zone_import_show(self, zone_import_id, *args, **kwargs): + cmd = 'zone import show {0}'.format(zone_import_id) + return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs) + + def zone_import_delete(self, zone_import_id, *args, **kwargs): + cmd = 'zone import delete {0}'.format(zone_import_id) + return self.parsed_cmd(cmd, *args, **kwargs) + + class RecordsetCommands(object): def recordset_show(self, zone_id, id, *args, **kwargs): @@ -285,8 +305,8 @@ class BlacklistCommands(object): class DesignateCLI(base.CLIClient, ZoneCommands, ZoneTransferCommands, - ZoneExportCommands, RecordsetCommands, TLDCommands, - BlacklistCommands): + ZoneExportCommands, ZoneImportCommands, RecordsetCommands, + TLDCommands, BlacklistCommands): # instantiate this once to minimize requests to keystone _CLIENTS = None diff --git a/designateclient/functionaltests/datagen.py b/designateclient/functionaltests/datagen.py index 2f89321..58f952c 100644 --- a/designateclient/functionaltests/datagen.py +++ b/designateclient/functionaltests/datagen.py @@ -35,3 +35,13 @@ def random_a_recordset_name(zone_name, recordset_name='testrecord'): def random_blacklist(name='testblacklist'): return '{0}{1}'.format(name, random_digits()) + + +def random_zone_file(name='testzoneimport'): + return "$ORIGIN {0}{1}.com.\n" \ + "$TTL 300\n" \ + "{0}{1}.com. 300 IN SOA ns.{0}{1}.com. nsadmin.{0}{1}.com. 42 42 42 42 42\n" \ + "{0}{1}.com. 300 IN NS ns.{0}{1}.com.\n" \ + "{0}{1}.com. 300 IN MX 10 mail.{0}{1}.com.\n" \ + "ns.{0}{1}.com. 300 IN A 10.0.0.1\n" \ + "mail.{0}{1}.com. 300 IN A 10.0.0.2\n".format(name, random_digits()) diff --git a/designateclient/functionaltests/v2/fixtures.py b/designateclient/functionaltests/v2/fixtures.py index 2e61148..75f4406 100644 --- a/designateclient/functionaltests/v2/fixtures.py +++ b/designateclient/functionaltests/v2/fixtures.py @@ -16,6 +16,7 @@ limitations under the License. from __future__ import absolute_import from __future__ import print_function import sys +import tempfile import traceback import fixtures @@ -123,6 +124,38 @@ class ExportFixture(BaseFixture): pass +class ImportFixture(BaseFixture): + """See DesignateCLI.zone_import_create for __init__ args""" + + def __init__(self, zone_file_contents, user='default', *args, **kwargs): + super(ImportFixture, self).__init__(user, *args, **kwargs) + self.zone_file_contents = zone_file_contents + + def _setUp(self): + super(ImportFixture, self)._setUp() + + with tempfile.NamedTemporaryFile() as f: + f.write(self.zone_file_contents) + f.flush() + + self.zone_import = self.client.zone_import_create( + zone_file_path=f.name, + *self.args, **self.kwargs + ) + + self.addCleanup(self.cleanup_zone_import, self.client, + self.zone_import.id) + self.addCleanup(ZoneFixture.cleanup_zone, self.client, + self.zone_import.zone_id) + + @classmethod + def cleanup_zone_import(cls, client, zone_import_id): + try: + client.zone_import_delete(zone_import_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 index fc10f85..6052a99 100644 --- a/designateclient/functionaltests/v2/test_zone_export.py +++ b/designateclient/functionaltests/v2/test_zone_export.py @@ -39,7 +39,7 @@ class TestZoneExport(BaseDesignateTest): zone_exports = self.clients.zone_export_list() self.assertGreater(len(zone_exports), 0) - self.assertTrue(self._is_export_in_list(zone_export, zone_exports)) + self.assertTrue(self._is_entity_in_list(zone_export, zone_exports)) def test_create_and_show_zone_export(self): zone_export = self.useFixture(ExportFixture( @@ -60,12 +60,12 @@ class TestZoneExport(BaseDesignateTest): )).zone_export zone_exports = self.clients.zone_export_list() - self.assertTrue(self._is_export_in_list(zone_export, zone_exports)) + self.assertTrue(self._is_entity_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)) + self.assertFalse(self._is_entity_in_list(zone_export, zone_exports)) def test_show_export_file(self): zone_export = self.useFixture(ExportFixture( @@ -79,16 +79,3 @@ class TestZoneExport(BaseDesignateTest): 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/functionaltests/v2/test_zone_import.py b/designateclient/functionaltests/v2/test_zone_import.py new file mode 100644 index 0000000..232b57f --- /dev/null +++ b/designateclient/functionaltests/v2/test_zone_import.py @@ -0,0 +1,63 @@ +""" +Copyright 2016 Rackspace + +Author: Rahman Syed + +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_file +from designateclient.functionaltests.v2.fixtures import ImportFixture + + +class TestZoneImport(BaseDesignateTest): + + def setUp(self): + super(TestZoneImport, self).setUp() + self.ensure_tld_exists('com') + self.zone_file_contents = random_zone_file() + + def test_list_zone_imports(self): + zone_import = self.useFixture(ImportFixture( + zone_file_contents=self.zone_file_contents + )).zone_import + + zone_imports = self.clients.zone_import_list() + self.assertGreater(len(zone_imports), 0) + self.assertTrue(self._is_entity_in_list(zone_import, zone_imports)) + + def test_create_and_show_zone_import(self): + zone_import = self.useFixture(ImportFixture( + zone_file_contents=self.zone_file_contents + )).zone_import + + fetched_import = self.clients.zone_import_show(zone_import.id) + + self.assertEqual(zone_import.created_at, fetched_import.created_at) + self.assertEqual(zone_import.id, fetched_import.id) + self.assertEqual(zone_import.project_id, fetched_import.project_id) + + self.assertEqual('COMPLETE', fetched_import.status) + + def test_delete_zone_import(self): + zone_import = self.useFixture(ImportFixture( + zone_file_contents=self.zone_file_contents + )).zone_import + + zone_imports = self.clients.zone_import_list() + self.assertTrue(self._is_entity_in_list(zone_import, zone_imports)) + + self.clients.zone_import_delete(zone_import.id) + + zone_imports = self.clients.zone_import_list() + self.assertFalse(self._is_entity_in_list(zone_import, zone_imports)) diff --git a/designateclient/tests/v2/test_zones.py b/designateclient/tests/v2/test_zones.py index 673615e..2723992 100644 --- a/designateclient/tests/v2/test_zones.py +++ b/designateclient/tests/v2/test_zones.py @@ -312,3 +312,57 @@ class TestZoneExports(v2.APIV2TestCase, v2.CrudMixin): response = self.client.zone_exports.get_export(ref["id"]) self.assertEqual(ref, response) + + +class TestZoneImports(v2.APIV2TestCase, v2.CrudMixin): + def new_ref(self, **kwargs): + ref = super(TestZoneImports, 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("message", 'Importing...') + ref.setdefault("version", '1') + return ref + + def test_create_import(self): + zonefile = '$ORIGIN example.com' + + parts = ["zones", "tasks", "imports"] + self.stub_url('POST', parts=parts, json=zonefile) + + self.client.zone_imports.create(zonefile) + self.assertRequestBodyIs(body=zonefile) + + def test_get_import(self): + ref = self.new_ref() + + parts = ["zones", "tasks", "imports", 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_imports.get_import_record(ref["id"]) + self.assertEqual(ref, response) + + def test_list_imports(self): + items = [ + self.new_ref(), + self.new_ref() + ] + + parts = ["zones", "tasks", "imports"] + self.stub_url('GET', parts=parts, json={"imports": items}) + + listed = self.client.zone_imports.list() + self.assertList(items, listed["imports"]) + self.assertQueryStringIs("") + + def test_delete_import(self): + ref = self.new_ref() + + parts = ["zones", "tasks", "imports", ref["id"]] + self.stub_url('DELETE', parts=parts, json=ref) + self.stub_entity("DELETE", parts=parts, id=ref["id"]) + + self.client.zone_imports.delete(ref["id"]) + self.assertRequestBodyIs(None) diff --git a/designateclient/v2/cli/zones.py b/designateclient/v2/cli/zones.py index 7f49671..234d910 100644 --- a/designateclient/v2/cli/zones.py +++ b/designateclient/v2/cli/zones.py @@ -37,6 +37,10 @@ def _format_zone_export_record(zone_export_record): zone_export_record.pop('links', None) +def _format_zone_import_record(zone_import_record): + zone_import_record.pop('links', None) + + class ListZonesCommand(lister.Lister): """List zones""" @@ -501,3 +505,96 @@ class ShowZoneExportFileCommand(show.ShowOne): data = client.zone_exports.get_export(parsed_args.zone_export_id) return ['data'], [data] + + +class ImportZoneCommand(show.ShowOne): + """Import a Zone from a file on the filesystem""" + + def get_parser(self, prog_name): + parser = super(ImportZoneCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_file_path', + help="Path to a zone file", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + with open(parsed_args.zone_file_path, 'r') as f: + zone_file_contents = f.read() + + data = client.zone_imports.create(zone_file_contents) + _format_zone_import_record(data) + + LOG.info('Zone Import %s was created', data['id']) + + return six.moves.zip(*sorted(six.iteritems(data))) + + +class ListZoneImportsCommand(lister.Lister): + """List Zone Imports""" + + columns = [ + 'id', + 'zone_id', + 'created_at', + 'status', + 'message', + ] + + def get_parser(self, prog_name): + parser = super(ListZoneImportsCommand, self).get_parser( + prog_name) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_imports.list() + + cols = self.columns + return cols, (utils.get_item_properties(s, cols) + for s in data['imports']) + + +class ShowZoneImportCommand(show.ShowOne): + """Show a Zone Import""" + + def get_parser(self, prog_name): + parser = super(ShowZoneImportCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_import_id', help="Zone Import ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + data = client.zone_imports.get_import_record( + parsed_args.zone_import_id) + _format_zone_import_record(data) + + return six.moves.zip(*sorted(six.iteritems(data))) + + +class DeleteZoneImportCommand(command.Command): + """Delete a Zone Import""" + + def get_parser(self, prog_name): + parser = super(DeleteZoneImportCommand, self).get_parser( + prog_name) + + parser.add_argument('zone_import_id', help="Zone Import ID", type=str) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.dns + + client.zone_imports.delete(parsed_args.zone_import_id) + + LOG.info('Zone Import %s was deleted', parsed_args.zone_import_id) diff --git a/designateclient/v2/zones.py b/designateclient/v2/zones.py index 8c200c8..fa9abad 100644 --- a/designateclient/v2/zones.py +++ b/designateclient/v2/zones.py @@ -147,4 +147,15 @@ class ZoneExportsController(V2Controller): class ZoneImportsController(V2Controller): - pass + def create(self, zone_file_contents): + return self._post('/zones/tasks/imports', data=zone_file_contents, + headers={'Content-Type': 'text/dns'}) + + def get_import_record(self, zone_import_id): + return self._get('/zones/tasks/imports/%s' % zone_import_id) + + def list(self): + return self._get('/zones/tasks/imports') + + def delete(self, zone_import_id): + return self._delete('/zones/tasks/imports/%s' % zone_import_id) diff --git a/setup.cfg b/setup.cfg index 1c9751d..df44212 100644 --- a/setup.cfg +++ b/setup.cfg @@ -115,6 +115,11 @@ openstack.dns.v2 = zone_export_delete = designateclient.v2.cli.zones:DeleteZoneExportCommand zone_export_showfile = designateclient.v2.cli.zones:ShowZoneExportFileCommand + zone_import_create = designateclient.v2.cli.zones:ImportZoneCommand + zone_import_list = designateclient.v2.cli.zones:ListZoneImportsCommand + zone_import_show = designateclient.v2.cli.zones:ShowZoneImportCommand + zone_import_delete = designateclient.v2.cli.zones:DeleteZoneImportCommand + zone_transfer_request_create = designateclient.v2.cli.zones:CreateTransferRequestCommand zone_transfer_request_list = designateclient.v2.cli.zones:ListTransferRequestsCommand zone_transfer_request_show = designateclient.v2.cli.zones:ShowTransferRequestCommand