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
This commit is contained in:
Rahman Syed 2016-03-22 17:08:18 -05:00
parent 575917105b
commit ce50ad9444
10 changed files with 375 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,6 +109,12 @@ openstack.dns.v2 =
zone_abandon = designateclient.v2.cli.zones:AbandonZoneCommand
zone_axfr = designateclient.v2.cli.zones:AXFRZoneCommand
zone_export_create = designateclient.v2.cli.zones:ExportZoneCommand
zone_export_list = designateclient.v2.cli.zones:ListZoneExportsCommand
zone_export_show = designateclient.v2.cli.zones:ShowZoneExportCommand
zone_export_delete = designateclient.v2.cli.zones:DeleteZoneExportCommand
zone_export_showfile = designateclient.v2.cli.zones:ShowZoneExportFileCommand
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