diff --git a/designatedashboard/api/rest/__init__.py b/designatedashboard/api/rest/__init__.py index 6e741ff..1e898c3 100644 --- a/designatedashboard/api/rest/__init__.py +++ b/designatedashboard/api/rest/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. """REST API for Horizon dashboard Javascript code. """ -from . import passthrough # noqa +from . import designate # noqa diff --git a/designatedashboard/api/rest/designate.py b/designatedashboard/api/rest/designate.py new file mode 100644 index 0000000..d78073e --- /dev/null +++ b/designatedashboard/api/rest/designate.py @@ -0,0 +1,272 @@ +# Copyright (c) 2023 Binero +# +# 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 designatedashboard.sdk_connection import get_sdk_connection +from django.views import generic +import logging + +from openstack_dashboard.api.rest import urls +from openstack_dashboard.api.rest import utils as rest_utils + +from openstack.dns.v2 import floating_ip as _fip + + +LOG = logging.getLogger(__name__) + + +def _sdk_object_to_list(object): + """Converts an SDK generator object to a list of dictionaries. + + :param object: SDK generator object + :returns: List of dictionaries + """ + result_list = [] + for item in object: + result_list.append(item.to_dict()) + return result_list + + +def create_zone(request): + """Create zone.""" + data = request.DATA + + conn = get_sdk_connection(request) + build_kwargs = dict( + name=data['name'], + email=data['email'], + type=data['type'], + ) + if data.get('description', None): + build_kwargs['description'] = data['description'] + if data.get('ttl', None): + build_kwargs['ttl'] = data['ttl'] + if data.get('masters', None): + build_kwargs['masters'] = data['masters'] + + zone = conn.dns.create_zone(**build_kwargs) + return zone.to_dict() + + +def update_zone(request, **kwargs): + """Update zone.""" + data = request.DATA + zone_id = kwargs.get('zone_id') + + conn = get_sdk_connection(request) + build_kwargs = dict( + email=data['email'], + description=data['description'], + ttl=data['ttl'], + ) + zone = conn.dns.update_zone( + zone_id, **build_kwargs) + return zone.to_dict() + + +@urls.register +class Zones(generic.View): + """API for zones.""" + + url_regex = r'dns/v2/zones/$' + + @rest_utils.ajax() + def get(self, request): + """List zones for current project.""" + conn = get_sdk_connection(request) + zones = _sdk_object_to_list(conn.dns.zones()) + return {'zones': zones} + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create zone.""" + return create_zone(request) + + +@urls.register +class Zone(generic.View): + """API for zone.""" + url_regex = r'dns/v2/zones/(?P[^/]+)/$' + + @rest_utils.ajax() + def get(self, request, zone_id): + """Get zone.""" + conn = get_sdk_connection(request) + zone = conn.dns.find_zone(zone_id) + return zone.to_dict() + + @rest_utils.ajax(data_required=True) + def patch(self, request, zone_id): + """Edit zone.""" + kwargs = {'zone_id': zone_id} + update_zone(request, **kwargs) + + @rest_utils.ajax() + def delete(self, request, zone_id): + """Delete zone.""" + conn = get_sdk_connection(request) + conn.dns.delete_zone(zone_id, ignore_missing=True) + + +def create_recordset(request, **kwargs): + """Create recordset.""" + data = request.DATA + zone_id = kwargs.get('zone_id') + + conn = get_sdk_connection(request) + build_kwargs = dict( + name=data['name'], + type=data['type'], + ttl=data['ttl'], + records=data['records'], + ) + if data.get('description', None): + build_kwargs['description'] = data['description'] + + rs = conn.dns.create_recordset( + zone_id, **build_kwargs) + return rs.to_dict() + + +def update_recordset(request, **kwargs): + """Update recordset.""" + data = request.DATA + zone_id = kwargs.get('zone_id') + rs_id = kwargs.get('rs_id') + + conn = get_sdk_connection(request) + + build_kwargs = dict() + if data.get('description', None): + build_kwargs['description'] = data['description'] + if data.get('ttl', None): + build_kwargs['ttl'] = data['ttl'] + if data.get('records', None): + build_kwargs['records'] = data['records'] + + build_kwargs['zone_id'] = zone_id + rs = conn.dns.update_recordset( + rs_id, **build_kwargs) + return rs.to_dict() + + +def _populate_zone_id(items, zone_id): + for item in items: + item['zone_id'] = zone_id + return items + + +@urls.register +class RecordSets(generic.View): + """API for recordsets.""" + url_regex = r'dns/v2/zones/(?P[^/]+)/recordsets/$' + + @rest_utils.ajax() + def get(self, request, zone_id): + """Get recordsets.""" + conn = get_sdk_connection(request) + rsets = _sdk_object_to_list(conn.dns.recordsets(zone_id)) + return {'recordsets': _populate_zone_id(rsets, zone_id)} + + @rest_utils.ajax(data_required=True) + def post(self, request, zone_id): + """Create recordset.""" + kwargs = {'zone_id': zone_id} + return create_recordset(request, **kwargs) + + +@urls.register +class RecordSet(generic.View): + """API for recordset.""" + url_regex = r'dns/v2/zones/(?P[^/]+)/recordsets/(?P[^/]+)/$' # noqa + + @rest_utils.ajax() + def get(self, request, zone_id, rs_id): + """Get recordset.""" + conn = get_sdk_connection(request) + rs = conn.dns.get_recordset(rs_id, zone_id) + rs_dict = rs.to_dict() + rs_dict['zone_id'] = zone_id + return rs_dict + + @rest_utils.ajax(data_required=True) + def put(self, request, zone_id, rs_id): + """Edit recordset.""" + kwargs = {'zone_id': zone_id, 'rs_id': rs_id} + update_recordset(request, **kwargs) + + @rest_utils.ajax() + def delete(self, request, zone_id, rs_id): + """Delete recordset.""" + conn = get_sdk_connection(request) + conn.dns.delete_recordset(rs_id, zone_id, ignore_missing=True) + + +@urls.register +class DnsFloatingIps(generic.View): + """API for floatingips.""" + url_regex = r'dns/v2/reverse/floatingips/$' + + @rest_utils.ajax() + def get(self, request): + """Get floatingips.""" + conn = get_sdk_connection(request) + fips = _sdk_object_to_list(conn.dns.floating_ips()) + return {'floatingips': fips} + + +def update_dns_floatingip(request, **kwargs): + """Update recordset.""" + data = request.DATA + fip_id = kwargs.get('fip_id') + + conn = get_sdk_connection(request) + + build_kwargs = dict( + ptrdname=data['ptrdname'], + ) + if data.get('description', None): + build_kwargs['description'] = data['description'] + if data.get('ttl', None): + build_kwargs['ttl'] = data['ttl'] + + # TODO(tobias-urdin): Bug in openstacksdk + # https://review.opendev.org/c/openstack/openstacksdk/+/903879 + obj = conn.dns._get_resource( + _fip.FloatingIP, fip_id, **build_kwargs) + obj.resource_key = None + has_body = True + if build_kwargs['ptrdname'] is None: + has_body = False + fip = obj.commit(conn.dns, has_body=has_body) + + return fip.to_dict() + + +@urls.register +class DnsFloatingIp(generic.View): + """API for dns floatingip.""" + url_regex = r'dns/v2/reverse/floatingips/(?P[^/]+)/$' + + @rest_utils.ajax() + def get(self, request, fip_id): + """Get floatingip.""" + conn = get_sdk_connection(request) + fip = conn.dns.get_floating_ip(fip_id) + return fip.to_dict() + + @rest_utils.ajax(data_required=True) + def patch(self, request, fip_id): + """Edit floatingip.""" + kwargs = {'fip_id': fip_id} + update_dns_floatingip(request, **kwargs) diff --git a/designatedashboard/api/rest/passthrough.py b/designatedashboard/api/rest/passthrough.py deleted file mode 100644 index 61d250e..0000000 --- a/designatedashboard/api/rest/passthrough.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2016, Hewlett Packard Enterprise Development, LP -# -# 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. -"""API for the passthrough service. -""" -from django.conf import settings -from django.views import generic -import functools -import logging -import requests -from requests.exceptions import HTTPError - -from horizon import exceptions -from openstack_dashboard.api import base -from openstack_dashboard.api.rest import urls -from openstack_dashboard.api.rest import utils as rest_utils - -LOG = logging.getLogger(__name__) - - -def _passthrough_request(request_method, url, - request, data=None, params=None): - """Makes a request to the appropriate service API with an optional payload. - - Should set any necessary auth headers and SSL parameters. - """ - - # Set verify if a CACERT is set and SSL_NO_VERIFY isn't True - verify = getattr(settings, 'OPENSTACK_SSL_CACERT', None) - if getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False): - verify = False - - service_url = _get_service_url(request, 'dns') - request_url = '{}{}'.format( - service_url, - url if service_url.endswith('/') else ('/' + url) - ) - - response = request_method( - request_url, - headers={'X-Auth-Token': request.user.token.id}, - json=data, - verify=verify, - params=params - ) - - try: - response.raise_for_status() - except HTTPError as e: - LOG.debug(e.response.content) - for error in rest_utils.http_errors: - if (e.response.status_code == getattr(error, 'status_code', 0) and - exceptions.HorizonException in error.__bases__): - raise error - raise - - return response - - -# Create some convenience partial functions -passthrough_get = functools.partial(_passthrough_request, requests.get) -passthrough_post = functools.partial(_passthrough_request, requests.post) -passthrough_put = functools.partial(_passthrough_request, requests.put) -passthrough_patch = functools.partial(_passthrough_request, requests.patch) -passthrough_delete = functools.partial(_passthrough_request, requests.delete) - - -def _get_service_url(request, service): - """Get service's URL from keystone; allow an override in settings""" - service_url = getattr(settings, service.upper() + '_URL', None) - try: - service_url = base.url_for(request, service) - except exceptions.ServiceCatalogException: - pass - # Currently the keystone endpoint is http://host:port/ - # without the version. - return service_url - - -@urls.register -class Passthrough(generic.View): - """Pass-through API for executing service requests. - - Horizon only adds auth and CORS proxying. - """ - url_regex = r'dns/(?P.+)$' - - @rest_utils.ajax() - def get(self, request, path): - return passthrough_get(path, request).json() - - @rest_utils.ajax() - def post(self, request, path): - data = dict(request.DATA) if request.DATA else {} - return passthrough_post(path, request, data).json() - - @rest_utils.ajax() - def put(self, request, path): - data = dict(request.DATA) if request.DATA else {} - return passthrough_put(path, request, data).json() - - @rest_utils.ajax() - def patch(self, request, path): - data = dict(request.DATA) if request.DATA else {} - return passthrough_patch(path, request, data).json() - - @rest_utils.ajax() - def delete(self, request, path): - return passthrough_delete(path, request).json() diff --git a/designatedashboard/sdk_connection.py b/designatedashboard/sdk_connection.py new file mode 100644 index 0000000..6132b71 --- /dev/null +++ b/designatedashboard/sdk_connection.py @@ -0,0 +1,50 @@ +# Copyright Red Hat +# +# 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 django.conf import settings + +import designatedashboard +from openstack import config as occ +from openstack import connection + + +def get_sdk_connection(request): + """Creates an SDK connection based on the request. + + :param request: Django request object + :returns: SDK connection object + """ + # NOTE(mordred) Nothing says love like two inverted booleans + # The config setting is NO_VERIFY which is, in fact, insecure. + # get_one_cloud wants verify, so we pass 'not insecure' to verify. + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + # Pass interface to honor 'OPENSTACK_ENDPOINT_TYPE' + interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'publicURL') + # Pass load_yaml_config as this is a Django service with its own config + # and we don't want to accidentally pick up a clouds.yaml file. We want to + # use the settings we're passing in. + cloud_config = occ.OpenStackConfig(load_yaml_config=False).get_one_cloud( + verify=not insecure, + cacert=cacert, + interface=interface, + region_name=request.user.services_region, + auth_type='token', + auth=dict( + project_id=request.user.project_id, + project_domain_id=request.user.domain_id, + auth_token=request.user.token.unscoped_token, + auth_url=request.user.endpoint), + app_name='designate-dashboard', + app_version=designatedashboard.__version__) + return connection.from_config(cloud_config=cloud_config) diff --git a/designatedashboard/static/designatedashboard/resources/os-designate-floatingip/api.service.js b/designatedashboard/static/designatedashboard/resources/os-designate-floatingip/api.service.js index 77d9a2c..afd6d49 100644 --- a/designatedashboard/static/designatedashboard/resources/os-designate-floatingip/api.service.js +++ b/designatedashboard/static/designatedashboard/resources/os-designate-floatingip/api.service.js @@ -61,7 +61,7 @@ */ function list(params) { var config = params ? {params: params} : {}; - return httpService.get(apiPassthroughUrl + 'v2/reverse/floatingips', config) + return httpService.get(apiPassthroughUrl + 'v2/reverse/floatingips/', config) .catch(function () { toastService.add('error', gettext('Unable to retrieve the floating ip PTRs.')); }); @@ -69,7 +69,7 @@ function get(id, params) { var config = params ? {params: params} : {}; - return httpService.get(apiPassthroughUrl + 'v2/reverse/floatingips/' + id, config) + return httpService.get(apiPassthroughUrl + 'v2/reverse/floatingips/' + id + '/', config) .catch(function () { toastService.add('error', gettext('Unable to get the floating ip PTR ' + id)); }); @@ -95,7 +95,7 @@ ttl: data.ttl }; return httpService.patch( - apiPassthroughUrl + 'v2/reverse/floatingips/' + floatingIpID, apiData) + apiPassthroughUrl + 'v2/reverse/floatingips/' + floatingIpID + '/', apiData) .catch(function () { toastService.add('error', gettext('Unable to set the floating IP PTR record.')); }); diff --git a/designatedashboard/static/designatedashboard/resources/os-designate-recordset/api.service.js b/designatedashboard/static/designatedashboard/resources/os-designate-recordset/api.service.js index 88da513..5105515 100644 --- a/designatedashboard/static/designatedashboard/resources/os-designate-recordset/api.service.js +++ b/designatedashboard/static/designatedashboard/resources/os-designate-recordset/api.service.js @@ -130,7 +130,7 @@ records: data.records }; return httpService.put( - apiPassthroughUrl + 'v2/zones/' + zoneId + '/recordsets/' + recordSetId, apiData) + apiPassthroughUrl + 'v2/zones/' + zoneId + '/recordsets/' + recordSetId + '/', apiData) .catch(function () { toastService.add('error', gettext('Unable to update the record set.')); }); diff --git a/releasenotes/notes/openstacksdk-11483491f9978bd1.yaml b/releasenotes/notes/openstacksdk-11483491f9978bd1.yaml new file mode 100644 index 0000000..99dcab5 --- /dev/null +++ b/releasenotes/notes/openstacksdk-11483491f9978bd1.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + The designate dashboard now needs openstacksdk diff --git a/requirements.txt b/requirements.txt index de5452e..8758041 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 horizon>=17.1.0 # Apache-2.0 +openstacksdk>=0.62.0 # Apache-2.0