diff --git a/designate/api/admin/controllers/extensions/export.py b/designate/api/admin/controllers/extensions/export.py new file mode 100644 index 00000000..ee4de7c0 --- /dev/null +++ b/designate/api/admin/controllers/extensions/export.py @@ -0,0 +1,61 @@ +# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P. +# +# 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 pecan +from oslo_log import log as logging + +from designate.api.v2.controllers import rest +from designate import utils +from designate import policy + +LOG = logging.getLogger(__name__) + + +class ExportController(rest.RestController): + + @pecan.expose(template=None, content_type='text/dns') + @utils.validate_uuid('zone_id') + def get_one(self, zone_id): + context = pecan.request.environ['context'] + + policy.check('zone_export', context) + + servers = self.central_api.get_domain_servers(context, zone_id) + domain = self.central_api.get_domain(context, zone_id) + + criterion = {'domain_id': zone_id} + recordsets = self.central_api.find_recordsets(context, criterion) + + records = [] + + for recordset in recordsets: + criterion = { + 'domain_id': domain['id'], + 'recordset_id': recordset['id'] + } + + raw_records = self.central_api.find_records(context, criterion) + + for record in raw_records: + records.append({ + 'name': recordset['name'], + 'type': recordset['type'], + 'ttl': recordset['ttl'], + 'data': record['data'], + }) + + return utils.render_template('bind9-zone.jinja2', + servers=servers, + domain=domain, + records=records) diff --git a/designate/api/admin/controllers/extensions/import_.py b/designate/api/admin/controllers/extensions/import_.py new file mode 100644 index 00000000..d041a89a --- /dev/null +++ b/designate/api/admin/controllers/extensions/import_.py @@ -0,0 +1,77 @@ +# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P. +# +# 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 dns import zone as dnszone +from dns import exception as dnsexception +import pecan +from oslo_log import log as logging +from oslo.config import cfg + +from designate.api.v2.controllers import rest +from designate import dnsutils +from designate import exceptions +from designate.objects.adapters import DesignateAdapter +from designate import policy + +LOG = logging.getLogger(__name__) + + +class ImportController(rest.RestController): + + BASE_URI = cfg.CONF['service:api'].api_base_uri.rstrip('/') + + @pecan.expose(template='json:', content_type='application/json') + def post_all(self): + request = pecan.request + response = pecan.response + context = pecan.request.environ['context'] + + policy.check('zone_import', context) + + try: + dnspython_zone = dnszone.from_text( + request.body, + # Don't relativize, otherwise we end up with '@' record names. + relativize=False, + # Dont check origin, we allow missing NS records (missing SOA + # records are taken care of in _create_zone). + check_origin=False) + domain = dnsutils.from_dnspython_zone(dnspython_zone) + domain.type = 'PRIMARY' + + for rrset in list(domain.recordsets): + if rrset.type in ('NS', 'SOA'): + domain.recordsets.remove(rrset) + + except dnszone.UnknownOrigin: + raise exceptions.BadRequest('The $ORIGIN statement is required and' + ' must be the first statement in the' + ' zonefile.') + except dnsexception.SyntaxError: + raise exceptions.BadRequest('Malformed zonefile.') + + zone = self.central_api.create_domain(context, domain) + + if zone['status'] == 'PENDING': + response.status_int = 202 + else: + response.status_int = 201 + + zone = DesignateAdapter.render('API_v2', zone, request=request) + + zone['links']['self'] = '%s%s/%s' % ( + self.BASE_URI, 'v2/zones', zone['id']) + + response.headers['Location'] = zone['links']['self'] + + return zone diff --git a/designate/api/admin/controllers/extensions/zones.py b/designate/api/admin/controllers/extensions/zones.py new file mode 100644 index 00000000..f994c729 --- /dev/null +++ b/designate/api/admin/controllers/extensions/zones.py @@ -0,0 +1,39 @@ +# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P. +# +# 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 oslo_log import log as logging + +from designate.api.v2.controllers import rest +from designate.api.admin.controllers.extensions import import_ +from designate.api.admin.controllers.extensions import export + +LOG = logging.getLogger(__name__) + + +class ZonesController(rest.RestController): + + @staticmethod + def get_path(): + return '.zones' + + def __init__(self): + # Import is a keyword - so we have to do a setattr instead + setattr(self, 'import', import_.ImportController()) + super(ZonesController, self).__init__() + + # We cannot do an assignment as import is a keyword. it is done as part of + # the __init__() above + # + # import = import_.CountsController() + export = export.ExportController() diff --git a/designate/api/v2/controllers/zones/__init__.py b/designate/api/v2/controllers/zones/__init__.py index 0148f920..09d7da66 100644 --- a/designate/api/v2/controllers/zones/__init__.py +++ b/designate/api/v2/controllers/zones/__init__.py @@ -14,13 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. import pecan -from dns import zone as dnszone -from dns import exception as dnsexception from oslo.config import cfg from designate import exceptions from designate import utils -from designate import dnsutils from designate.api.v2.controllers import rest from designate.api.v2.controllers import recordsets from designate.api.v2.controllers.zones import tasks @@ -40,7 +37,6 @@ class ZonesController(rest.RestController): tasks = tasks.TasksController() @pecan.expose(template='json:', content_type='application/json') - @pecan.expose(template=None, content_type='text/dns') @utils.validate_uuid('zone_id') def get_one(self, zone_id): """Get Zone""" @@ -48,56 +44,12 @@ class ZonesController(rest.RestController): request = pecan.request context = request.environ['context'] - if 'Accept' not in request.headers: - raise exceptions.BadRequest('Missing Accept header') - best_match = request.accept.best_match(['application/json', - 'text/dns']) - if best_match == 'text/dns': - return self._get_zonefile(request, context, zone_id) - elif best_match == 'application/json': - return self._get_json(request, context, zone_id) - else: - raise exceptions.UnsupportedAccept( - 'Accept must be text/dns or application/json') - def _get_json(self, request, context, zone_id): - """'Normal' zone get""" return DesignateAdapter.render( 'API_v2', self.central_api.get_domain(context, zone_id), request=request) - def _get_zonefile(self, request, context, zone_id): - """Export zonefile""" - servers = self.central_api.get_domain_servers(context, zone_id) - domain = self.central_api.get_domain(context, zone_id) - - criterion = {'domain_id': zone_id} - recordsets = self.central_api.find_recordsets(context, criterion) - - records = [] - - for recordset in recordsets: - criterion = { - 'domain_id': domain['id'], - 'recordset_id': recordset['id'] - } - - raw_records = self.central_api.find_records(context, criterion) - - for record in raw_records: - records.append({ - 'name': recordset['name'], - 'type': recordset['type'], - 'ttl': recordset['ttl'], - 'data': record['data'], - }) - - return utils.render_template('bind9-zone.jinja2', - servers=servers, - domain=domain, - records=records) - @pecan.expose(template='json:', content_type='application/json') def get_all(self, **params): """List Zones""" @@ -125,16 +77,7 @@ class ZonesController(rest.RestController): request = pecan.request response = pecan.response context = request.environ['context'] - if request.content_type == 'text/dns': - return self._post_zonefile(request, response, context) - elif request.content_type == 'application/json': - return self._post_json(request, response, context) - else: - raise exceptions.UnsupportedContentType( - 'Content-type must be text/dns or application/json') - def _post_json(self, request, response, context): - """'Normal' zone creation""" zone = request.body_dict # We need to check the zone type before validating the schema since if @@ -175,43 +118,6 @@ class ZonesController(rest.RestController): return zone - def _post_zonefile(self, request, response, context): - """Import Zone""" - try: - dnspython_zone = dnszone.from_text( - request.body, - # Don't relativize, otherwise we end up with '@' record names. - relativize=False, - # Dont check origin, we allow missing NS records (missing SOA - # records are taken care of in _create_zone). - check_origin=False) - domain = dnsutils.from_dnspython_zone(dnspython_zone) - domain.type = 'PRIMARY' - - for rrset in list(domain.recordsets): - if rrset.type in ('NS', 'SOA'): - domain.recordsets.remove(rrset) - - except dnszone.UnknownOrigin: - raise exceptions.BadRequest('The $ORIGIN statement is required and' - ' must be the first statement in the' - ' zonefile.') - except dnsexception.SyntaxError: - raise exceptions.BadRequest('Malformed zonefile.') - - zone = self.central_api.create_domain(context, domain) - - if zone['status'] == 'PENDING': - response.status_int = 202 - else: - response.status_int = 201 - - zone = DesignateAdapter.render('API_v2', zone, request=request) - - response.headers['Location'] = zone['links']['self'] - - return zone - @pecan.expose(template='json:', content_type='application/json') @pecan.expose(template='json:', content_type='application/json-patch+json') @utils.validate_uuid('zone_id') diff --git a/designate/api/v2/patches.py b/designate/api/v2/patches.py index a90474d2..79eee1e3 100644 --- a/designate/api/v2/patches.py +++ b/designate/api/v2/patches.py @@ -41,7 +41,8 @@ class Request(pecan.core.Request): except ValueError as valueError: raise exceptions.InvalidJson(valueError.message) else: - raise Exception('TODO: Unsupported Content Type') + raise exceptions.UnsupportedContentType( + 'Content-type must be application/json') __init__ = pecan.core.Pecan.__base__.__init__ if not ismethod(__init__) or 'request_cls' not in getargspec(__init__).args: diff --git a/designate/tests/test_api/test_admin/extensions/test_zones.py b/designate/tests/test_api/test_admin/extensions/test_zones.py new file mode 100644 index 00000000..8917349e --- /dev/null +++ b/designate/tests/test_api/test_admin/extensions/test_zones.py @@ -0,0 +1,81 @@ +# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P. +# +# 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 dns import zone as dnszone +from oslo.config import cfg + +from designate.tests.test_api.test_admin import AdminApiTestCase + +cfg.CONF.import_opt('enabled_extensions_admin', 'designate.api.admin', + group='service:api') + + +class AdminApiZoneImportExportTest(AdminApiTestCase): + def setUp(self): + self.config(enabled_extensions_admin=['zones'], group='service:api') + super(AdminApiZoneImportExportTest, self).setUp() + + # Zone import/export + def test_missing_origin(self): + self.policy({'zone_import': '@'}) + fixture = self.get_zonefile_fixture(variant='noorigin') + + self._assert_exception('bad_request', 400, self.client.post, + '/zones/import', + fixture, headers={'Content-type': 'text/dns'}) + + def test_missing_soa(self): + self.policy({'zone_import': '@'}) + fixture = self.get_zonefile_fixture(variant='nosoa') + + self._assert_exception('bad_request', 400, self.client.post, + '/zones/import', + fixture, headers={'Content-type': 'text/dns'}) + + def test_malformed_zonefile(self): + self.policy({'zone_import': '@'}) + fixture = self.get_zonefile_fixture(variant='malformed') + + self._assert_exception('bad_request', 400, self.client.post, + '/zones/import', + fixture, headers={'Content-type': 'text/dns'}) + + def test_import_export(self): + self.policy({'default': '@'}) + # Since v2 doesn't support getting records, import and export the + # fixture, making sure they're the same according to dnspython + post_response = self.client.post('/zones/import', + self.get_zonefile_fixture(), + headers={'Content-type': 'text/dns'}) + get_response = self.client.get('/zones/export/%s' % + post_response.json['id'], + headers={'Accept': 'text/dns'}) + + exported_zonefile = get_response.body + imported = dnszone.from_text(self.get_zonefile_fixture()) + exported = dnszone.from_text(exported_zonefile) + # Compare SOA emails, since zone comparison takes care of origin + imported_soa = imported.get_rdataset(imported.origin, 'SOA') + imported_email = imported_soa[0].rname.to_text() + exported_soa = exported.get_rdataset(exported.origin, 'SOA') + exported_email = exported_soa[0].rname.to_text() + self.assertEqual(imported_email, exported_email) + # Delete SOAs since they have, at the very least, different serials, + # and dnspython considers that to be not equal. + imported.delete_rdataset(imported.origin, 'SOA') + exported.delete_rdataset(exported.origin, 'SOA') + # Delete NS records, since they won't be the same + imported.delete_rdataset(imported.origin, 'NS') + exported.delete_rdataset(exported.origin, 'NS') + imported.delete_rdataset('delegation', 'NS') + self.assertEqual(imported, exported) diff --git a/designate/tests/test_api/test_v2/test_zones.py b/designate/tests/test_api/test_v2/test_zones.py index 3fc6573e..a47f9650 100644 --- a/designate/tests/test_api/test_v2/test_zones.py +++ b/designate/tests/test_api/test_v2/test_zones.py @@ -13,7 +13,6 @@ # 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 dns import zone as dnszone from mock import patch from oslo.config import cfg from oslo import messaging @@ -234,11 +233,6 @@ class ApiV2ZonesTest(ApiV2TestCase): self._assert_exception('domain_not_found', 404, self.client.get, url, headers={'Accept': 'application/json'}) - def test_get_zone_missing_accept(self): - url = '/zones/6e2146f3-87bc-4f47-adc5-4df0a5c78218' - - self._assert_exception('bad_request', 400, self.client.get, url) - def test_get_zone_bad_accept(self): url = '/zones/6e2146f3-87bc-4f47-adc5-4df0a5c78218' @@ -480,54 +474,6 @@ class ApiV2ZonesTest(ApiV2TestCase): self._assert_exception('invalid_object', 400, self.client.patch_json, '/zones/%s' % zone['id'], body) - # Zone import/export - def test_missing_origin(self): - fixture = self.get_zonefile_fixture(variant='noorigin') - - self._assert_exception('bad_request', 400, self.client.post, '/zones', - fixture, headers={'Content-type': 'text/dns'}) - - def test_missing_soa(self): - fixture = self.get_zonefile_fixture(variant='nosoa') - - self._assert_exception('bad_request', 400, self.client.post, '/zones', - fixture, headers={'Content-type': 'text/dns'}) - - def test_malformed_zonefile(self): - fixture = self.get_zonefile_fixture(variant='malformed') - - self._assert_exception('bad_request', 400, self.client.post, '/zones', - fixture, headers={'Content-type': 'text/dns'}) - - def test_import_export(self): - # Since v2 doesn't support getting records, import and export the - # fixture, making sure they're the same according to dnspython - post_response = self.client.post('/zones', - self.get_zonefile_fixture(), - headers={'Content-type': 'text/dns'}) - get_response = self.client.get('/zones/%s' % - post_response.json['id'], - headers={'Accept': 'text/dns'}) - - exported_zonefile = get_response.body - imported = dnszone.from_text(self.get_zonefile_fixture()) - exported = dnszone.from_text(exported_zonefile) - # Compare SOA emails, since zone comparison takes care of origin - imported_soa = imported.get_rdataset(imported.origin, 'SOA') - imported_email = imported_soa[0].rname.to_text() - exported_soa = exported.get_rdataset(exported.origin, 'SOA') - exported_email = exported_soa[0].rname.to_text() - self.assertEqual(imported_email, exported_email) - # Delete SOAs since they have, at the very least, different serials, - # and dnspython considers that to be not equal. - imported.delete_rdataset(imported.origin, 'SOA') - exported.delete_rdataset(exported.origin, 'SOA') - # Delete NS records, since they won't be the same - imported.delete_rdataset(imported.origin, 'NS') - exported.delete_rdataset(exported.origin, 'NS') - imported.delete_rdataset('delegation', 'NS') - self.assertEqual(imported, exported) - # Metadata tests def test_metadata_exists(self): response = self.client.get('/zones/') diff --git a/doc/source/rest/admin/zones.rst b/doc/source/rest/admin/zones.rst new file mode 100644 index 00000000..1eee6d6c --- /dev/null +++ b/doc/source/rest/admin/zones.rst @@ -0,0 +1,114 @@ +Zones +===== + +Overview +-------- +The zones extension can be used to import and export zonesfiles to designate. + +*Note*: Zones is an extension and needs to be enabled before it can be used. +If Designate returns a 404 error, ensure that the following line has been +added to the designate.conf file:: + + enabled_extensions_admin = zones + +Once this line has been added, restart the designate-api service. + +Export Zone +----------- + +.. http:get:: /admin/zones/export/(uuid:id) + + **Example request:** + + .. sourcecode:: http + + GET /admin/zones/export/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1 + Host: 127.0.0.1:9001 + Accept: text/dns + + + **Example response:** + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/dns + + $ORIGIN example.com. + $TTL 42 + + example.com. IN SOA ns.designate.com. nsadmin.example.com. ( + 1394213803 ; serial + 3600 ; refresh + 600 ; retry + 86400 ; expire + 3600 ; minimum + ) + + + example.com. IN NS ns.designate.com. + + + example.com. IN MX 10 mail.example.com. + ns.example.com. IN A 10.0.0.1 + mail.example.com. IN A 10.0.0.2 + + :statuscode 200: Success + :statuscode 406: Not Acceptable + + Notice how the SOA and NS records are replaced with the Designate server(s). + +Import Zone +----------- + +.. http:post:: /admin/zones/import + + To import a zonefile, set the Content-type to **text/dns** . The + **zoneextractor.py** tool in the **contrib** folder can generate zonefiles + that are suitable for Designate (without any **$INCLUDE** statements for + example). + + **Example request:** + + .. sourcecode:: http + + POST /admin/zones/import HTTP/1.1 + Host: 127.0.0.1:9001 + Content-type: text/dns + + $ORIGIN example.com. + example.com. 42 IN SOA ns.example.com. nsadmin.example.com. 42 42 42 42 42 + example.com. 42 IN NS ns.example.com. + example.com. 42 IN MX 10 mail.example.com. + ns.example.com. 42 IN A 10.0.0.1 + mail.example.com. 42 IN A 10.0.0.2 + + **Example response:** + + .. sourcecode:: http + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "email": "nsadmin@example.com", + "id": "6b78734a-aef1-45cd-9708-8eb3c2d26ff1", + "links": { + "self": "http://127.0.0.1:9001/v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff1" + }, + "name": "example.com.", + "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", + "project_id": "d7accc2f8ce343318386886953f2fc6a", + "serial": 1404757531, + "ttl": "42", + "created_at": "2014-07-07T18:25:31.275934", + "updated_at": null, + "version": 1, + "masters": [], + "type": "PRIMARY", + "transferred_at": null + } + + :statuscode 201: Created + :statuscode 415: Unsupported Media Type + :statuscode 400: Bad request diff --git a/doc/source/rest/v2/zones.rst b/doc/source/rest/v2/zones.rst index 0e88ebed..9b22b83b 100644 --- a/doc/source/rest/v2/zones.rst +++ b/doc/source/rest/v2/zones.rst @@ -291,106 +291,6 @@ Delete Zone :statuscode 202: Accepted -Import Zone ------------ - -.. http:post:: /zones - - To import a zonefile, set the Content-type to **text/dns** . The - **zoneextractor.py** tool in the **contrib** folder can generate zonefiles - that are suitable for Designate (without any **$INCLUDE** statements for - example). - - **Example request:** - - .. sourcecode:: http - - POST /v2/zones HTTP/1.1 - Host: 127.0.0.1:9001 - Content-type: text/dns - - $ORIGIN example.com. - example.com. 42 IN SOA ns.example.com. nsadmin.example.com. 42 42 42 42 42 - example.com. 42 IN NS ns.example.com. - example.com. 42 IN MX 10 mail.example.com. - ns.example.com. 42 IN A 10.0.0.1 - mail.example.com. 42 IN A 10.0.0.2 - - **Example response:** - - .. sourcecode:: http - - HTTP/1.1 201 Created - Content-Type: application/json - - { - "email": "nsadmin@example.com", - "id": "6b78734a-aef1-45cd-9708-8eb3c2d26ff1", - "links": { - "self": "http://127.0.0.1:9001/v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff1" - }, - "name": "example.com.", - "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2", - "project_id": "d7accc2f8ce343318386886953f2fc6a", - "serial": 1404757531, - "ttl": "42", - "created_at": "2014-07-07T18:25:31.275934", - "updated_at": null, - "version": 1, - "masters": [], - "type": "PRIMARY", - "transferred_at": null - } - - :statuscode 201: Created - :statuscode 415: Unsupported Media Type - :statuscode 400: Bad request - -Export Zone ------------ - -.. http:get:: /zones/(uuid:id) - - To export a zone in zonefile format, set the **Accept** header to **text/dns**. - - **Example request** - - .. sourcecode:: http - - GET /v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff1 HTTP/1.1 - Host: 127.0.0.1:9001 - Accept: text/dns - - **Example response** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: text/dns - - $ORIGIN example.com. - $TTL 42 - - example.com. IN SOA ns.designate.com. nsadmin.example.com. ( - 1394213803 ; serial - 3600 ; refresh - 600 ; retry - 86400 ; expire - 3600 ; minimum - ) - - - example.com. IN NS ns.designate.com. - - - example.com. IN MX 10 mail.example.com. - ns.example.com. IN A 10.0.0.1 - mail.example.com. IN A 10.0.0.2 - - :statuscode 200: Success - :statuscode 406: Not Acceptable - - Notice how the SOA and NS records are replaced with the Designate server(s). Abandon Zone ------------ diff --git a/etc/designate/designate.conf.sample b/etc/designate/designate.conf.sample index c12be2d2..5cf84510 100644 --- a/etc/designate/designate.conf.sample +++ b/etc/designate/designate.conf.sample @@ -101,7 +101,8 @@ debug = False #enable_api_admin = False # Enabled Admin API extensions -# Can be one or more of : reports, quotas, counts, tenants +# Can be one or more of : reports, quotas, counts, tenants, zones +# zone import / export is in zones extension #enabled_extensions_admin = # Show the pecan HTML based debug interface (v2 only) diff --git a/etc/designate/policy.json b/etc/designate/policy.json index a085e2da..c259e884 100644 --- a/etc/designate/policy.json +++ b/etc/designate/policy.json @@ -19,6 +19,9 @@ "use_low_ttl": "rule:admin", + "zone_import": "rule:admin", + "zone_export": "rule:admin", + "get_quotas": "rule:admin_or_owner", "get_quota": "rule:admin_or_owner", "set_quota": "rule:admin", diff --git a/setup.cfg b/setup.cfg index 74f98a8e..e5c5fcb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,7 @@ designate.api.v1.extensions = designate.api.admin.extensions = reports = designate.api.admin.controllers.extensions.reports:ReportsController quotas = designate.api.admin.controllers.extensions.quotas:QuotasController + zones = designate.api.admin.controllers.extensions.zones:ZonesController designate.storage = sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage