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:
parent
483e0d16c6
commit
bc39d23ff5
@ -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
|
||||
|
@ -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
|
||||
|
73
designateclient/functionaltests/v2/test_shared_zone.py
Normal file
73
designateclient/functionaltests/v2/test_shared_zone.py
Normal 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))
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}')
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user