Specify database instance access during creation

Change-Id: I75950ae949e7f19e48f5246e4db9f5d5c514dd2b
This commit is contained in:
Ivan Kolodyazhny 2021-02-02 22:44:08 +02:00
parent 2db165a81a
commit c9d6d03a0f
6 changed files with 72 additions and 11 deletions

View File

@ -7,3 +7,4 @@ oslo.log>=3.30.0 # Apache-2.0
python-swiftclient>=2.2.0 python-swiftclient>=2.2.0
python-troveclient>=1.2.0 python-troveclient>=1.2.0
horizon>=17.1.0 # Apache-2.0 horizon>=17.1.0 # Apache-2.0
netaddr>=0.7.18 # BSD

View File

@ -149,7 +149,7 @@ def instance_create(request, name, volume, flavor=None, databases=None,
datastore=None, datastore_version=None, datastore=None, datastore_version=None,
replica_of=None, replica_count=None, replica_of=None, replica_count=None,
volume_type=None, configuration=None, locality=None, volume_type=None, configuration=None, locality=None,
availability_zone=None): availability_zone=None, access=None):
# TODO(dklyle): adding conditional to support trove without volume # TODO(dklyle): adding conditional to support trove without volume
# support for now until API supports checking for volume support # support for now until API supports checking for volume support
if volume > 0 and not replica_of: if volume > 0 and not replica_of:
@ -179,7 +179,8 @@ def instance_create(request, name, volume, flavor=None, databases=None,
replica_count=replica_count, replica_count=replica_count,
configuration=configuration, configuration=configuration,
locality=locality, locality=locality,
availability_zone=availability_zone) availability_zone=availability_zone,
access=access)
def instance_resize_volume(request, instance_id, size): def instance_resize_volume(request, instance_id, size):

View File

@ -214,7 +214,7 @@ class DatabasesBackupsTests(test.TestCase):
url = RESTORE_URL + '?backup=%s' % self.database_backups.first().id url = RESTORE_URL + '?backup=%s' % self.database_backups.first().id
res = self.client.get(url) res = self.client.get(url)
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_check, 4, mock.call((), test.IsHttpRequest())) self.mock_check, 5, mock.call((), test.IsHttpRequest()))
self.mock_backup_get.assert_called_once_with( self.mock_backup_get.assert_called_once_with(
test.IsHttpRequest(), test.IsA(str)) test.IsHttpRequest(), test.IsA(str))
self.mock_backup_list.assert_called_once_with(test.IsHttpRequest()) self.mock_backup_list.assert_called_once_with(test.IsHttpRequest())

View File

@ -0,0 +1,4 @@
{% load i18n %}
<p>{% blocktrans trimmed %}Access defines how the database service is exposed.{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}CIDRs is a comma-separated list of IPv4, IPv6 or mix of both CIDRs that restrict access to the database service. 0.0.0.0/0 is used by default if this parameter is not provided. E.g.: 0.0.0.0/0,202.78.240.0/24{% endblocktrans %}</p>

View File

@ -163,7 +163,7 @@ class DatabaseTests(test.TestCase):
res = self.client.get(LAUNCH_URL) res = self.client.get(LAUNCH_URL)
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_check, 4, mock.call((), test.IsHttpRequest())) self.mock_check, 5, mock.call((), test.IsHttpRequest()))
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_datastore_flavors, 20, self.mock_datastore_flavors, 20,
mock.call(test.IsHttpRequest(), mock.call(test.IsHttpRequest(),
@ -269,7 +269,7 @@ class DatabaseTests(test.TestCase):
res = self.client.post(LAUNCH_URL, post) res = self.client.post(LAUNCH_URL, post)
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_check, 4, mock.call((), test.IsHttpRequest())) self.mock_check, 5, mock.call((), test.IsHttpRequest()))
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_datastore_flavors, 20, self.mock_datastore_flavors, 20,
mock.call(test.IsHttpRequest(), mock.call(test.IsHttpRequest(),
@ -306,7 +306,8 @@ class DatabaseTests(test.TestCase):
replica_count=None, replica_count=None,
volume_type=None, volume_type=None,
locality=None, locality=None,
availability_zone=test.IsA(str)) availability_zone=test.IsA(str),
access=None)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_mocks({ @test.create_mocks({
@ -359,7 +360,7 @@ class DatabaseTests(test.TestCase):
res = self.client.post(LAUNCH_URL, post) res = self.client.post(LAUNCH_URL, post)
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_check, 4, mock.call((), test.IsHttpRequest())) self.mock_check, 5, mock.call((), test.IsHttpRequest()))
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_datastore_flavors, 20, self.mock_datastore_flavors, 20,
mock.call(test.IsHttpRequest(), mock.call(test.IsHttpRequest(),
@ -396,7 +397,8 @@ class DatabaseTests(test.TestCase):
replica_count=None, replica_count=None,
volume_type=None, volume_type=None,
locality=None, locality=None,
availability_zone=test.IsA(str)) availability_zone=test.IsA(str),
access=None)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_mocks({ @test.create_mocks({
@ -1093,7 +1095,7 @@ class DatabaseTests(test.TestCase):
res = self.client.post(LAUNCH_URL, post) res = self.client.post(LAUNCH_URL, post)
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_check, 4, mock.call((), test.IsHttpRequest())) self.mock_check, 5, mock.call((), test.IsHttpRequest()))
self.assert_mock_multiple_calls_with_same_arguments( self.assert_mock_multiple_calls_with_same_arguments(
self.mock_datastore_flavors, 20, self.mock_datastore_flavors, 20,
mock.call(test.IsHttpRequest(), mock.call(test.IsHttpRequest(),
@ -1133,7 +1135,8 @@ class DatabaseTests(test.TestCase):
replica_count=2, replica_count=2,
volume_type=None, volume_type=None,
locality=None, locality=None,
availability_zone=test.IsA(str)) availability_zone=test.IsA(str),
access=None)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_mocks({ @test.create_mocks({

View File

@ -15,6 +15,7 @@
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import netaddr
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
@ -283,6 +284,44 @@ class SetInstanceDetails(workflows.Step):
"locality", "availability_zone") "locality", "availability_zone")
class AddAccessAction(workflows.Action):
"""Initialize the database access. This tab will honor
the settings which should be a list of permissions required:
* TROVE_ADD_USER_PERMS = []
* TROVE_ADD_DATABASE_PERMS = []
"""
is_public = forms.BooleanField(label=_("Is Public"),
required=False)
allowed_cidrs = forms.CharField(label=_("Allowed CIDRs"),
required=False,
help_text=_("Comma-separated CIDRs "
"to connect through."))
class Meta(object):
name = _("Database Access")
permissions = TROVE_ADD_PERMS
help_text_template = "project/databases/_launch_access_help.html"
def clean(self):
cleaned_data = super(AddAccessAction, self).clean()
if cleaned_data.get('allowed_cidrs'):
cidrs = cleaned_data.get('allowed_cidrs').split(',')
for cidr in cidrs:
try:
netaddr.IPNetwork(cidr)
except netaddr.AddrFormatError:
msg = _('Invalid Allowed CIDR provided.')
self._errors["allowed_cidrs"] = self.error_class([msg])
return cleaned_data
class DatabaseAccess(workflows.Step):
action_class = AddAccessAction
contributes = ["is_public", "allowed_cidrs"]
class AddDatabasesAction(workflows.Action): class AddDatabasesAction(workflows.Action):
"""Initialize the database with users/databases. This tab will honor """Initialize the database with users/databases. This tab will honor
the settings which should be a list of permissions required: the settings which should be a list of permissions required:
@ -513,6 +552,7 @@ class LaunchInstance(workflows.Workflow):
success_url = "horizon:project:databases:index" success_url = "horizon:project:databases:index"
default_steps = (SetInstanceDetails, default_steps = (SetInstanceDetails,
dash_create_instance.SetNetwork, dash_create_instance.SetNetwork,
DatabaseAccess,
InitializeDatabase, InitializeDatabase,
Advanced) Advanced)
@ -577,6 +617,16 @@ class LaunchInstance(workflows.Workflow):
locality = context['locality'] locality = context['locality']
return locality return locality
def _get_access(self, context):
if not context['allowed_cidrs'] and not context['is_public']:
return None
access = {}
if context['allowed_cidrs'] != '':
access['allowed_cidrs'].split(',')
if context['is_public']:
access['is_public'] = True
return access
def handle(self, request, context): def handle(self, request, context):
try: try:
datastore, datastore_version = parse_datastore_and_version_text( datastore, datastore_version = parse_datastore_and_version_text(
@ -597,6 +647,7 @@ class LaunchInstance(workflows.Workflow):
context.get('master'), context['replica_count'], context.get('master'), context['replica_count'],
context.get('config'), self._get_locality(context), context.get('config'), self._get_locality(context),
avail_zone) avail_zone)
api.trove.instance_create(request, api.trove.instance_create(request,
context['name'], context['name'],
context['volume'], context['volume'],
@ -613,7 +664,8 @@ class LaunchInstance(workflows.Workflow):
context), context),
configuration=context.get('config'), configuration=context.get('config'),
locality=self._get_locality(context), locality=self._get_locality(context),
availability_zone=avail_zone) availability_zone=avail_zone,
access=self._get_access(context))
return True return True
except Exception: except Exception:
exceptions.handle(request) exceptions.handle(request)