Implement zone import

Zone import commands (create, list, show, delete)
are missing from the python-designateclient.

This change includes implementation, as well as unit tests
and functionaltests.

Change-Id: I635431b5ef11989247e8d185ad692e4fee9bebba
Closes-Bug: #1550532
This commit is contained in:
Rahman Syed 2016-03-24 19:21:31 -05:00
parent ce50ad9444
commit d2b88c88c5
11 changed files with 316 additions and 20 deletions

View File

@ -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):

View File

@ -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])

View File

@ -173,6 +173,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):
@ -283,8 +303,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

View File

@ -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())

View File

@ -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"""

View File

@ -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])

View File

@ -0,0 +1,63 @@
"""
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_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))

View File

@ -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)

View File

@ -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"""
@ -498,3 +502,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)

View File

@ -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)

View File

@ -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