Merge "Migrate passthrough to openstacksdk"

This commit is contained in:
Zuul 2024-01-22 23:06:49 +00:00 committed by Gerrit Code Review
commit eaa6b7c3ba
8 changed files with 332 additions and 124 deletions

View File

@ -13,4 +13,4 @@
# limitations under the License.
"""REST API for Horizon dashboard Javascript code.
"""
from . import passthrough # noqa
from . import designate # noqa

View File

@ -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<zone_id>[^/]+)/$'
@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<zone_id>[^/]+)/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<zone_id>[^/]+)/recordsets/(?P<rs_id>[^/]+)/$' # 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<fip_id>[^/]+)/$'
@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)

View File

@ -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<path>.+)$'
@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()

View File

@ -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)

View File

@ -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.'));
});

View File

@ -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.'));
});

View File

@ -0,0 +1,4 @@
---
upgrade:
- |
The designate dashboard now needs openstacksdk

View File

@ -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