diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index e85db5dbd..8c53c5418 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -60,3 +60,11 @@ Zone Transfer Operations create_zone_transfer_request, update_zone_transfer_request, delete_zone_transfer_request, zone_transfer_accepts, get_zone_transfer_accept, create_zone_transfer_accept + +Zone Share Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: create_zone_share, delete_zone_share, get_zone_share, + find_zone_share, zone_shares diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index a8d0c9360..77860a6a0 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -8,5 +8,6 @@ DNS Resources v2/zone_transfer v2/zone_export v2/zone_import + v2/zone_share v2/floating_ip v2/recordset diff --git a/doc/source/user/resources/dns/v2/zone_share.rst b/doc/source/user/resources/dns/v2/zone_share.rst new file mode 100644 index 000000000..5b0d02836 --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone_share.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.zone_share +=========================== + +.. automodule:: openstack.dns.v2.zone_share + +The ZoneShare Class +------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_share.ZoneShare + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 1f147b7e3..85bc1cbb0 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -15,6 +15,7 @@ from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export from openstack.dns.v2 import zone_import as _zone_import +from openstack.dns.v2 import zone_share as _zone_share from openstack.dns.v2 import zone_transfer as _zone_transfer from openstack import proxy @@ -26,6 +27,7 @@ class Proxy(proxy.Proxy): "zone": _zone.Zone, "zone_export": _zone_export.ZoneExport, "zone_import": _zone_import.ZoneImport, + "zone_share": _zone_share.ZoneShare, "zone_transfer_request": _zone_transfer.ZoneTransferRequest, } @@ -69,7 +71,7 @@ class Proxy(proxy.Proxy): """ return self._get(_zone.Zone, zone) - def delete_zone(self, zone, ignore_missing=True): + def delete_zone(self, zone, ignore_missing=True, delete_shares=False): """Delete a zone :param zone: The value can be the ID of a zone @@ -79,11 +81,14 @@ class Proxy(proxy.Proxy): the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. + :param bool delete_shares: When True, delete the zone shares along with + the zone. :returns: Zone been deleted :rtype: :class:`~openstack.dns.v2.zone.Zone` """ - return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing) + return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing, + delete_shares=delete_shares) def update_zone(self, zone, **attrs): """Update zone attributes @@ -536,6 +541,93 @@ class Proxy(proxy.Proxy): """ return self._create(_zone_transfer.ZoneTransferAccept, **attrs) + # ======== Zone Shares ======== + def zone_shares(self, zone, **query): + """Retrieve a generator of zone sharess + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `target_project_id`: The target project ID field. + + :returns: A generator of zone shares + :class:`~openstack.dns.v2.zone_share.ZoneShare` instances. + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._list(_zone_share.ZoneShare, zone_id=zone_obj.id, **query) + + def get_zone_share(self, zone, zone_share): + """Get a zone share + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param zone_share: The zone_share can be either the ID of the zone + share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance + that the zone share belongs to. + + :returns: ZoneShare instance. + :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._get(_zone_share.ZoneShare, zone_share, + zone_id=zone_obj.id) + + def find_zone_share(self, zone, zone_share_id, ignore_missing=True): + """Find a single zone share + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param zone_share_id: The zone share ID + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the zone share does not exist. + When set to ``True``, None will be returned when attempting to + find a nonexistent zone share. + + :returns: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._find(_zone_share.ZoneShare, zone_share_id, + ignore_missing=ignore_missing, zone_id=zone_obj.id) + + def create_zone_share(self, zone, **attrs): + """Create a new zone share from attributes + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_share.ZoneShare`, + comprised of the properties on the ZoneShare class. + + :returns: The results of zone share creation + :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._create(_zone_share.ZoneShare, zone_id=zone_obj.id, + **attrs) + + def delete_zone_share(self, zone, zone_share, ignore_missing=True): + """Delete a zone share + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param zone_share: The zone_share can be either the ID of the zone + share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance + that the zone share belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone share does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone share. + + :returns: ``None`` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + self._delete(_zone_share.ZoneShare, zone_share, + ignore_missing=ignore_missing, zone_id=zone_obj.id) + def _get_cleanup_dependencies(self): # DNS may depend on floating ip return { diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py index bc8c4c09b..c794d9e0b 100644 --- a/openstack/dns/v2/zone.py +++ b/openstack/dns/v2/zone.py @@ -79,6 +79,13 @@ class Zone(_base.Resource): type = resource.Body('type') #: Timestamp when the zone was last updated updated_at = resource.Body('updated_at') + #: Whether the zone is shared with other projects + #: *Type: bool* + is_shared = resource.Body('shared') + + # Headers for DELETE requests + #: If true, delete any existing zone shares along with the zone + delete_shares = resource.Header('x-designate-delete-shares', type=bool) def _action(self, session, action, body): """Preform actions given the message body. diff --git a/openstack/dns/v2/zone_share.py b/openstack/dns/v2/zone_share.py new file mode 100644 index 000000000..b778b1620 --- /dev/null +++ b/openstack/dns/v2/zone_share.py @@ -0,0 +1,45 @@ +# 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 openstack.dns.v2 import _base +from openstack import resource + + +class ZoneShare(_base.Resource): + """DNS ZONE Share Resource""" + + resources_key = 'shared_zones' + base_path = '/zones/%(zone_id)s/shares' + + # capabilities + allow_create = True + allow_delete = True + allow_fetch = True + allow_list = True + + _query_mapping = resource.QueryParameters('target_project_id') + + # Properties + #: Timestamp when the share was created. + created_at = resource.Body('created_at') + #: Timestamp when the member was last updated. + updated_at = resource.Body('updated_at') + #: The zone ID of the zone being shared. + zone_id = resource.Body('zone_id') + #: The project ID that owns the share. + project_id = resource.Body('project_id') + #: The target project ID that the zone is shared with. + target_project_id = resource.Body('target_project_id') + + # URI Properties + #: The ID of the zone being shared. + zone_id = resource.URI('zone_id') diff --git a/openstack/dns/version.py b/openstack/dns/version.py new file mode 100644 index 000000000..e9bd971a3 --- /dev/null +++ b/openstack/dns/version.py @@ -0,0 +1,25 @@ +# 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 openstack import resource + + +class Version(resource.Resource): + resource_key = 'version' + resources_key = 'versions' + base_path = '/' + + # capabilities + allow_list = True + + # Properties + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 8acf77330..147b161f7 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -12,6 +12,7 @@ import random from openstack import connection +from openstack import exceptions from openstack.tests.functional import base @@ -66,3 +67,25 @@ class TestZone(base.BaseFunctionalTest): ttl=3600, records=['192.168.1.1'] )) + + def test_delete_zone_with_shares(self): + zone_name = 'example-{0}.org.'.format(random.randint(1, 10000)) + zone = self.conn.dns.create_zone( + name=zone_name, + email='joe@example.org', + type='PRIMARY', + ttl=7200, + description='example zone' + ) + self.addCleanup(self.conn.dns.delete_zone, zone) + + demo_project_id = self.operator_cloud.get_project('demo')['id'] + zone_share = self.conn.dns.create_zone_share( + zone, target_project_id=demo_project_id) + self.addCleanup(self.conn.dns.delete_zone_share, zone, zone_share) + + # Test that we cannot delete a zone with shares + self.assertRaises(exceptions.BadRequestException, + self.conn.dns.delete_zone, zone) + + self.conn.dns.delete_zone(zone, delete_shares=True) diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py new file mode 100644 index 000000000..1473ae46d --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -0,0 +1,119 @@ +# 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. +import random + +from openstack import exceptions +from openstack.tests.functional import base + + +class TestZoneShare(base.BaseFunctionalTest): + + def setUp(self): + super(TestZoneShare, self).setUp() + self.require_service('dns') + if not self.user_cloud: + self.skipTest("The demo cloud is required for this test") + + # Note: zone deletion is not an immediate operation, so each time + # chose a new zone name for a test + # getUniqueString is not guaranteed to return unique string between + # different tests of the same class. + self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 10000)) + + self.zone = self.operator_cloud.dns.create_zone( + name=self.ZONE_NAME, + email='joe@example.org', + type='PRIMARY', + ttl=7200, + description='example zone for sdk zone share tests' + ) + self.addCleanup(self.operator_cloud.dns.delete_zone, self.zone, + delete_shares=True) + + self.project_id = self.operator_cloud.session.get_project_id() + self.demo_project_id = self.user_cloud.session.get_project_id() + + def test_create_delete_zone_share(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertIsNotNone(zone_share.id) + self.assertIsNotNone(zone_share.created_at) + self.assertIsNone(zone_share.updated_at) + + def test_get_zone_share(self): + orig_zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + orig_zone_share) + + zone_share = self.operator_cloud.dns.get_zone_share(self.zone, + orig_zone_share) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertEqual(orig_zone_share.id, zone_share.id) + self.assertEqual(orig_zone_share.created_at, zone_share.created_at) + self.assertEqual(orig_zone_share.updated_at, zone_share.updated_at) + + def test_find_zone_share(self): + orig_zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + orig_zone_share) + + zone_share = self.operator_cloud.dns.find_zone_share( + self.zone, orig_zone_share.id) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertEqual(orig_zone_share.id, zone_share.id) + self.assertEqual(orig_zone_share.created_at, zone_share.created_at) + self.assertEqual(orig_zone_share.updated_at, zone_share.updated_at) + + def test_find_zone_share_ignore_missing(self): + zone_share = self.operator_cloud.dns.find_zone_share(self.zone, + 'bogus_id') + self.assertIsNone(zone_share) + + def test_find_zone_share_ignore_missing_false(self): + self.assertRaises(exceptions.ResourceNotFound, + self.operator_cloud.dns.find_zone_share, + self.zone, 'bogus_id', ignore_missing=False) + + def test_list_zone_shares(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + target_ids = [o.target_project_id for o in + self.operator_cloud.dns.zone_shares(self.zone)] + self.assertIn(self.demo_project_id, target_ids) + + def test_list_zone_shares_with_target_id(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + target_ids = [o.target_project_id for o in + self.operator_cloud.dns.zone_shares( + self.zone, target_project_id=self.demo_project_id)] + self.assertIn(self.demo_project_id, target_ids) diff --git a/openstack/tests/unit/dns/test_version.py b/openstack/tests/unit/dns/test_version.py new file mode 100644 index 000000000..be84a03bb --- /dev/null +++ b/openstack/tests/unit/dns/test_version.py @@ -0,0 +1,42 @@ +# 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 openstack.dns import version +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': '2', + 'status': '3', +} + + +class TestVersion(base.TestCase): + + def test_basic(self): + sot = version.Version() + self.assertEqual('version', sot.resource_key) + self.assertEqual('versions', sot.resources_key) + self.assertEqual('/', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = version.Version(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index ebabc8034..f80c56c34 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -16,6 +16,7 @@ from openstack.dns.v2 import recordset from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export from openstack.dns.v2 import zone_import +from openstack.dns.v2 import zone_share from openstack.dns.v2 import zone_transfer from openstack.tests.unit import test_proxy_base @@ -34,8 +35,9 @@ class TestDnsZone(TestDnsProxy): 'prepend_key': False}) def test_zone_delete(self): - self.verify_delete(self.proxy.delete_zone, - zone.Zone, True) + self.verify_delete( + self.proxy.delete_zone, zone.Zone, True, + expected_kwargs={'ignore_missing': True, 'delete_shares': False}) def test_zone_find(self): self.verify_find(self.proxy.find_zone, zone.Zone) @@ -213,3 +215,40 @@ class TestDnsZoneTransferAccept(TestDnsProxy): def test_zone_transfer_accept_create(self): self.verify_create(self.proxy.create_zone_transfer_accept, zone_transfer.ZoneTransferAccept) + + +class TestDnsZoneShare(TestDnsProxy): + def test_zone_share_create(self): + self.verify_create(self.proxy.create_zone_share, zone_share.ZoneShare, + method_kwargs={'zone': 'bogus_id'}, + expected_kwargs={'zone_id': 'bogus_id'}) + + def test_zone_share_delete(self): + self.verify_delete( + self.proxy.delete_zone_share, zone_share.ZoneShare, + ignore_missing=True, + method_args={'zone': 'bogus_id', 'zone_share': 'bogus_id'}, + expected_args=['zone_share'], + expected_kwargs={'zone_id': 'zone', 'ignore_missing': True}) + + def test_zone_share_find(self): + self.verify_find( + self.proxy.find_zone_share, zone_share.ZoneShare, + method_args=['zone'], + expected_args=['zone'], + expected_kwargs={'zone_id': 'resource_name', + 'ignore_missing': True}) + + def test_zone_share_get(self): + self.verify_get( + self.proxy.get_zone_share, zone_share.ZoneShare, + method_args=['zone', 'zone_share'], + expected_args=['zone_share'], + expected_kwargs={'zone_id': 'zone'}) + + def test_zone_shares(self): + self.verify_list( + self.proxy.zone_shares, zone_share.ZoneShare, + method_args=['zone'], + expected_args=[], + expected_kwargs={'zone_id': 'zone'}) diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py index 190e71a83..cbc41c699 100644 --- a/openstack/tests/unit/dns/v2/test_zone.py +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -28,7 +28,8 @@ EXAMPLE = { 'type': 'PRIMARY', 'ttl': 7200, 'description': 'This is an example zone.', - 'status': 'ACTIVE' + 'status': 'ACTIVE', + 'shared': False } @@ -76,6 +77,7 @@ class TestZone(base.TestCase): self.assertEqual(EXAMPLE['type'], sot.type) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['shared'], sot.is_shared) def test_abandon(self): sot = zone.Zone(**EXAMPLE) diff --git a/openstack/tests/unit/dns/v2/test_zone_share.py b/openstack/tests/unit/dns/v2/test_zone_share.py new file mode 100644 index 000000000..0c5d04ce7 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_share.py @@ -0,0 +1,63 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.dns.v2 import zone_share +from openstack.tests.unit import base + + +class TestZoneShare(base.TestCase): + + def setUp(self): + super(TestZoneShare, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_basic(self): + sot = zone_share.ZoneShare() + self.assertEqual(None, sot.resource_key) + self.assertEqual('shared_zones', sot.resources_key) + self.assertEqual('/zones/%(zone_id)s/shares', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertFalse(sot.allow_commit) + + self.assertDictEqual({'target_project_id': 'target_project_id', + 'limit': 'limit', 'marker': 'marker'}, + sot._query_mapping._mapping) + + def test_make_it(self): + share_id = 'bogus_id' + zone_id = 'bogus_zone_id' + project_id = 'bogus_project_id' + target_id = 'bogus_target_id' + expected = { + 'id': share_id, + 'zone_id': zone_id, + 'project_id': project_id, + 'target_project_id': target_id} + + sot = zone_share.ZoneShare(**expected) + self.assertEqual(share_id, sot.id) + self.assertEqual(zone_id, sot.zone_id) + self.assertEqual(project_id, sot.project_id) + self.assertEqual(target_id, sot.target_project_id) diff --git a/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml b/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml new file mode 100644 index 000000000..1541c1744 --- /dev/null +++ b/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add Designate (DNS) support for zone shares.