Add shared zone commands

Co-Authored-By: Michael Johnson <johnsomor@gmail.com>
Change-Id: Iea92371176d9126205384624a18a9097acb3daef
Partial-Bug: #1714088
Depends-On: https://review.opendev.org/#/c/726334/
This commit is contained in:
Igor Malinovskiy 2020-04-26 20:25:50 +03:00 committed by Michael Johnson
parent 483e0d16c6
commit bc39d23ff5
9 changed files with 376 additions and 4 deletions

View File

@ -348,9 +348,28 @@ class BlacklistCommands(object):
return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
class SharedZoneCommands(object):
def shared_zone_show(self, zone_id, shared_zone_id, *args, **kwargs):
cmd = 'zone share show {0} {1}'.format(zone_id, shared_zone_id)
return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
def shared_zone_list(self, zone_id, *args, **kwargs):
cmd = 'zone share list {0}'.format(zone_id)
return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
def share_zone(self, zone_id, target_project_id, *args, **kwargs):
cmd = 'zone share create {0} {1}'.format(zone_id, target_project_id)
return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
def unshare_zone(self, zone_id, shared_zone_id, *args, **kwargs):
cmd = 'zone share delete {0} {1}'.format(zone_id, shared_zone_id)
return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
class DesignateCLI(base.CLIClient, ZoneCommands, ZoneTransferCommands,
ZoneExportCommands, ZoneImportCommands, RecordsetCommands,
TLDCommands, BlacklistCommands):
TLDCommands, BlacklistCommands, SharedZoneCommands):
# instantiate this once to minimize requests to keystone
_CLIENTS = None

View File

@ -228,3 +228,25 @@ class BlacklistFixture(BaseFixture):
client.zone_blacklist_delete(blacklist_id)
except CommandFailed:
pass
class SharedZoneFixture(BaseFixture):
"""See DesignateCLI.recordset_create for __init__ args"""
def __init__(self, zone, *args, **kwargs):
super(SharedZoneFixture, self).__init__(*args, **kwargs)
self.zone = zone
def _setUp(self):
super(SharedZoneFixture, self)._setUp()
self.zone_share = self.client.zone_share(zone_id=self.zone.id,
*self.args, **self.kwargs)
self.addCleanup(self.cleanup_shared_zone, self.client, self.zone.id,
self.zone_share.id)
@classmethod
def cleanup_shared_zone(cls, client, zone_id, shared_zone_id):
try:
client.unshare_zone(zone_id, shared_zone_id)
except CommandFailed:
pass

View File

@ -0,0 +1,73 @@
"""
Copyright 2020 Cloudification GmbH. All rights reserved.
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.client import DesignateCLI
from designateclient.functionaltests.datagen import random_zone_name
from designateclient.functionaltests.v2.fixtures import SharedZoneFixture
from designateclient.functionaltests.v2.fixtures import ZoneFixture
class TestSharedZone(BaseDesignateTest):
def setUp(self):
super(TestSharedZone, self).setUp()
self.ensure_tld_exists('com')
fixture = self.useFixture(ZoneFixture(
name=random_zone_name(),
email='test@example.com',
))
self.zone = fixture.zone
self.target_client = DesignateCLI.as_user('alt')
def test_list_shared_zones(self):
shared_zone = self.useFixture(SharedZoneFixture(
zone_id=self.zone.id,
target_tenant_id=self.target_client.project_id
)).zone_share
shared_zones = self.clients.shared_zone_list(self.zone.id)
self.assertGreater(len(shared_zones), 0)
self.assertTrue(self._is_entity_in_list(shared_zone, shared_zones))
def test_share_and_show_shared_zone(self):
shared_zone = self.useFixture(SharedZoneFixture(
zone_id=self.zone.id,
target_tenant_id=self.target_client.project_id
)).zone_share
fetched_shared_zone = self.clients.shared_zone_show(self.zone.id,
shared_zone.id)
self.assertEqual(
shared_zone.created_at, fetched_shared_zone.created_at)
self.assertEqual(shared_zone.id, fetched_shared_zone.id)
self.assertEqual(
shared_zone.project_id, fetched_shared_zone.project_id)
self.assertEqual(shared_zone.zone_id, fetched_shared_zone.zone_id)
def test_unshare_zone(self):
shared_zone = self.useFixture(SharedZoneFixture(
zone_id=self.zone.id,
target_tenant_id=self.target_client.project_id
)).zone_share
shared_zones = self.clients.shared_zone_list(self.zone.id)
self.assertTrue(self._is_entity_in_list(shared_zone, shared_zones))
self.clients.unshare_zone(self.zone.id, shared_zone.id)
shared_zones = self.clients.shared_zone_list(self.zone.id)
self.assertFalse(self._is_entity_in_list(shared_zone, shared_zones))

View File

@ -116,7 +116,19 @@ class TestZones(v2.APIV2TestCase, v2.CrudMixin):
self.stub_entity("DELETE", id=ref["id"])
self.client.zones.delete(ref["id"])
self.assertRequestBodyIs(None)
self.assertRequestHeaderEqual('X-Designate-Delete-Shares', None)
def test_delete_with_delete_shares(self):
ref = self.new_ref()
self.stub_entity("DELETE", id=ref["id"])
self.client.zones.delete(ref["id"], delete_shares=True)
self.assertRequestBodyIs(None)
self.assertRequestHeaderEqual('X-Designate-Delete-Shares', 'true')
def test_task_abandon(self):
ref = self.new_ref()
@ -380,3 +392,73 @@ class TestZoneImports(v2.APIV2TestCase, v2.CrudMixin):
self.client.zone_imports.delete(ref["id"])
self.assertRequestBodyIs(None)
class TestZoneShared(v2.APIV2TestCase, v2.CrudMixin):
def setUp(self):
super(TestZoneShared, self).setUp()
self.zone_id = str(uuid.uuid4())
self.target_project_id = str(uuid.uuid4())
self.project_id = str(uuid.uuid4())
self.created_at = time.strftime("%c")
self.updated_at = time.strftime("%c")
def new_ref(self, **kwargs):
ref = super(TestZoneShared, self).new_ref(**kwargs)
ref.setdefault("zone_id", self.zone_id)
ref.setdefault("target_project_id", self.target_project_id)
ref.setdefault("project_id", self.project_id)
ref.setdefault("created_at", self.created_at)
ref.setdefault("updated_at", self.updated_at)
return ref
def test_share_a_zone(self):
json_body = {"target_project_id": self.target_project_id}
expected = self.new_ref()
self.stub_entity('POST', parts=['zones', self.zone_id, 'shares'],
entity=expected, json=json_body)
response = self.client.zone_share.create(self.zone_id,
self.target_project_id)
self.assertRequestBodyIs(json=json_body)
self.assertEqual(expected, response)
def test_get_zone_share(self):
expected = self.new_ref()
parts = ["zones", self.zone_id, "shares"]
self.stub_entity("GET", parts=parts, entity=expected,
id=expected["id"])
response = self.client.zone_share.get(self.zone_id, expected["id"])
self.assertRequestBodyIs(None)
self.assertEqual(expected, response)
def test_list_zone_shares(self):
items = [
self.new_ref(),
self.new_ref()
]
parts = ["zones", self.zone_id, "shares"]
self.stub_entity('GET', parts=parts, entity={"shared_zones": items})
listed = self.client.zone_share.list(self.zone_id)
self.assertList(items, listed)
self.assertQueryStringIs("")
def test_delete_zone_share(self):
ref = self.new_ref()
parts = ["zones", self.zone_id, "shares", ref["id"]]
self.stub_url('DELETE', parts=parts)
response = self.client.zone_share.delete(self.zone_id, ref["id"])
self.assertRequestBodyIs(None)
self.assertEqual('', response)

View File

@ -241,6 +241,10 @@ class DeleteZoneCommand(command.ShowOne):
parser.add_argument('id', help="Zone ID")
parser.add_argument('--delete-shares', default=False,
action='store_true',
help='Delete existing zone shares. Default: False')
common.add_all_common_options(parser)
common.add_hard_delete_option(parser)
@ -250,7 +254,13 @@ class DeleteZoneCommand(command.ShowOne):
client = self.app.client_manager.dns
common.set_all_common_headers(client, parsed_args)
data = client.zones.delete(parsed_args.id)
delete_shares = False
if (hasattr(parsed_args, 'delete_shares') and
parsed_args.delete_shares is not None and
isinstance(parsed_args.delete_shares, bool)):
delete_shares = parsed_args.delete_shares
data = client.zones.delete(parsed_args.id, delete_shares=delete_shares)
LOG.info('Zone %s was deleted', parsed_args.id)
_format_zone(data)
@ -724,3 +734,124 @@ class DeleteZoneImportCommand(command.Command):
client.zone_imports.delete(parsed_args.zone_import_id)
LOG.info('Zone Import %s was deleted', parsed_args.zone_import_id)
class ShareZoneCommand(command.ShowOne):
"""Share a Zone"""
def get_parser(self, prog_name):
parser = super(ShareZoneCommand, self).get_parser(
prog_name)
common.add_all_common_options(parser)
parser.add_argument('zone', help='The zone name or ID to share.')
parser.add_argument('target_project_id',
help='Target project ID to share the zone with.')
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.dns
common.set_all_common_headers(client, parsed_args)
data = client.zone_share.create(
parsed_args.zone,
parsed_args.target_project_id
)
LOG.info('Zone %s was shared', data['id'])
data.pop('links', None)
return self.dict2columns(data)
class ListSharedZonesCommand(command.Lister):
"""List Zone Shares"""
columns = [
'id',
'zone_id',
'target_project_id',
]
def get_parser(self, prog_name):
parser = super(ListSharedZonesCommand, self).get_parser(
prog_name)
common.add_all_common_options(parser)
parser.add_argument('zone', help='The zone name or ID to share.')
parser.add_argument('--target-project-id',
help='The target project ID to filter on.',
required=False)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.dns
common.set_all_common_headers(client, parsed_args)
criterion = {}
if parsed_args.target_project_id is not None:
criterion['target_project_id'] = parsed_args.target_project_id
data = get_all(client.zone_share.list, criterion=criterion,
args=[parsed_args.zone])
cols = list(self.columns)
if client.session.all_projects:
cols.insert(1, 'project_id')
return cols, (utils.get_item_properties(s, cols) for s in data)
class ShowSharedZoneCommand(command.ShowOne):
"""Show Zone Share Details"""
def get_parser(self, prog_name):
parser = super(ShowSharedZoneCommand, self).get_parser(prog_name)
parser.add_argument('zone', help='The zone name or ID to share.')
parser.add_argument('shared_zone_id',
help='The zone share ID to show.')
common.add_all_common_options(parser)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.dns
common.set_all_common_headers(client, parsed_args)
data = client.zone_share.get(parsed_args.zone,
parsed_args.shared_zone_id)
data.pop('links', None)
return self.dict2columns(data)
class DeleteSharedZoneCommand(command.Command):
"""Delete a Zone Share"""
def get_parser(self, prog_name):
parser = super(DeleteSharedZoneCommand, self).get_parser(
prog_name)
parser.add_argument('zone', help='The zone name or ID to share.')
parser.add_argument('shared_zone_id',
help='The zone share ID to delete.')
common.add_all_common_options(parser)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.dns
common.set_all_common_headers(client, parsed_args)
client.zone_share.delete(parsed_args.zone, parsed_args.shared_zone_id)
LOG.info('Shared Zone %s was deleted', parsed_args.shared_zone_id)

View File

@ -29,6 +29,7 @@ from designateclient.v2.tsigkeys import TSIGKeysController
from designateclient.v2.zones import ZoneController
from designateclient.v2.zones import ZoneExportsController
from designateclient.v2.zones import ZoneImportsController
from designateclient.v2.zones import ZoneShareController
from designateclient.v2.zones import ZoneTransfersController
from designateclient import version
from oslo_utils import importutils
@ -151,6 +152,7 @@ class Client(object):
self.zone_transfers = ZoneTransfersController(self)
self.zone_exports = ZoneExportsController(self)
self.zone_imports = ZoneImportsController(self)
self.zone_share = ZoneShareController(self)
self.pools = PoolController(self)
self.quotas = QuotasController(self)
self.tsigkeys = TSIGKeysController(self)

View File

@ -62,12 +62,18 @@ class ZoneController(V2Controller):
return self._patch(url, data=values)
def delete(self, zone):
def delete(self, zone, delete_shares=False):
zone = v2_utils.resolve_by_name(self.list, zone)
url = self.build_url('/zones/%s' % zone)
return self._delete(url)
if delete_shares:
headers = {'X-Designate-Delete-Shares': 'true'}
_resp, body = self.client.session.delete(url, headers=headers)
else:
_resp, body = self.client.session.delete(url)
return body
def abandon(self, zone):
zone = v2_utils.resolve_by_name(self.list, zone)
@ -166,3 +172,29 @@ class ZoneImportsController(V2Controller):
def delete(self, zone_import_id):
return self._delete('/zones/tasks/imports/%s' % zone_import_id)
class ZoneShareController(V2Controller):
def create(self, zone, target_project_id):
zone_id = v2_utils.resolve_by_name(self.client.zones.list, zone)
data = {"target_project_id": target_project_id}
return self._post(f'/zones/{zone_id}/shares', data=data)
def list(self, zone, criterion=None, marker=None, limit=None):
zone_id = v2_utils.resolve_by_name(self.client.zones.list, zone)
url = self.build_url(f'/zones/{zone_id}/shares',
criterion, marker, limit)
return self._get(url, response_key='shared_zones')
def delete(self, zone, shared_zone_id):
zone_id = v2_utils.resolve_by_name(self.client.zones.list, zone)
return self._delete(f'/zones/{zone_id}/shares/{shared_zone_id}')
def get(self, zone, shared_zone_id):
zone_id = v2_utils.resolve_by_name(self.client.zones.list, zone)
return self._get(f'/zones/{zone_id}/shares/{shared_zone_id}')

View File

@ -0,0 +1,6 @@
---
features:
- Adds zone share commands to support sharing zones with additional projects.
- Adds a ``--delete-shares`` option to zone delete to delete existing zone
shares along with the zone. Without this option, you cannot delete a zone
that has been shared with other projects.

View File

@ -74,6 +74,11 @@ openstack.dns.v2 =
zone_transfer_accept_list = designateclient.v2.cli.zones:ListTransferAcceptsCommand
zone_transfer_accept_show = designateclient.v2.cli.zones:ShowTransferAcceptCommand
zone_share_create = designateclient.v2.cli.zones:ShareZoneCommand
zone_share_list = designateclient.v2.cli.zones:ListSharedZonesCommand
zone_share_show = designateclient.v2.cli.zones:ShowSharedZoneCommand
zone_share_delete = designateclient.v2.cli.zones:DeleteSharedZoneCommand
recordset_create = designateclient.v2.cli.recordsets:CreateRecordSetCommand
recordset_list = designateclient.v2.cli.recordsets:ListRecordSetsCommand
recordset_show = designateclient.v2.cli.recordsets:ShowRecordSetCommand