Displays selectable ports in a tabular pop-up
This commit adds a new customizable Django/Horizon widget which displays Select options as table without adding AngularJS components or new custom JS code. The widget is then used to display port information in the dialogs used for port adding to, and removing from, Firewall Groups. Change-Id: I9707179557919643d4432d8ed29f2c80e44e6af4 Closes-Bug: #1810391
This commit is contained in:
parent
acf3f91833
commit
cb7c8c449a
|
@ -1,4 +1,3 @@
|
|||
#
|
||||
# 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
|
||||
|
@ -14,8 +13,10 @@
|
|||
import collections
|
||||
|
||||
from openstack_dashboard.api import neutron
|
||||
import openstack_dashboard.api.nova as nova
|
||||
from openstack_dashboard.contrib.developer.profiler import api as profiler
|
||||
|
||||
|
||||
neutronclient = neutron.neutronclient
|
||||
|
||||
|
||||
|
@ -57,11 +58,56 @@ def rule_create(request, **kwargs):
|
|||
return Rule(rule)
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def get_network_names(request):
|
||||
networks = neutronclient(request).list_networks(fields=["name", "id"])\
|
||||
.get('networks', [])
|
||||
mapped = {n['id']: neutron.Network(n) for n in networks}
|
||||
return mapped
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def get_router_names(request):
|
||||
routers = neutronclient(request).list_routers(fields=["name", "id"])\
|
||||
.get('routers', [])
|
||||
mapped = {r['id']: neutron.Router(r) for r in routers}
|
||||
return mapped
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def get_servers(request):
|
||||
servers = nova.server_list(request)[0]
|
||||
mapped = {s.id: s for s in servers}
|
||||
return mapped
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def rule_list(request, **kwargs):
|
||||
return _rule_list(request, **kwargs)
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def port_list(request, tenant_id, **kwargs):
|
||||
kwargs['tenant_id'] = tenant_id
|
||||
ports = neutronclient(request).list_ports(**kwargs).get('ports')
|
||||
|
||||
return {
|
||||
p['id']: Port(p) for p in ports if _is_target(p)
|
||||
}
|
||||
|
||||
|
||||
# Gets ids of all ports assigned to firewall groups
|
||||
@profiler.trace
|
||||
def fwg_port_list(request, **kwargs):
|
||||
fwgs = neutronclient(request).list_fwaas_firewall_groups(
|
||||
**kwargs).get('firewall_groups')
|
||||
ports = set()
|
||||
for fwg in fwgs:
|
||||
if fwg['ports']:
|
||||
ports.update(fwg['ports'])
|
||||
return ports
|
||||
|
||||
|
||||
@profiler.trace
|
||||
def fwg_port_list_for_tenant(request, tenant_id, **kwargs):
|
||||
kwargs['tenant_id'] = tenant_id
|
||||
|
|
|
@ -23,6 +23,7 @@ from horizon import messages
|
|||
from horizon.utils import validators
|
||||
|
||||
from neutron_fwaas_dashboard.api import fwaas_v2 as api_fwaas_v2
|
||||
from neutron_fwaas_dashboard.dashboards.project.firewalls_v2 import widgets
|
||||
|
||||
port_validator = validators.validate_port_or_colon_separated_port_range
|
||||
|
||||
|
@ -205,27 +206,72 @@ class UpdateFirewall(forms.SelfHandlingForm):
|
|||
exceptions.handle(request, msg, redirect=redirect)
|
||||
|
||||
|
||||
class AddPort(forms.SelfHandlingForm):
|
||||
failure_url = 'horizon:project:firewalls_v2:index'
|
||||
port_id = forms.ThemableChoiceField(
|
||||
label=_("Ports"), required=False)
|
||||
class PortSelectionForm(forms.SelfHandlingForm):
|
||||
port_id = forms.ThemableDynamicChoiceField(
|
||||
label=_("Ports"),
|
||||
required=False,
|
||||
widget=widgets.TableSelectWidget(
|
||||
columns=['Port', 'Network', 'Owner', 'Device'],
|
||||
alternate_xs=True
|
||||
)
|
||||
)
|
||||
|
||||
networks = {}
|
||||
routers = {}
|
||||
servers = {}
|
||||
ports = {}
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(AddPort, self).__init__(request, *args, **kwargs)
|
||||
super(PortSelectionForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
tenant_id = self.request.user.tenant_id
|
||||
ports = api_fwaas_v2.fwg_port_list_for_tenant(request, tenant_id)
|
||||
initial_ports = self.initial['ports']
|
||||
filtered_ports = [port for port in ports
|
||||
if port.id not in initial_ports]
|
||||
filtered_ports = sorted(filtered_ports, key=attrgetter('name'))
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to retrieve port list.'))
|
||||
ports = []
|
||||
tenant_id = self.request.user.tenant_id
|
||||
|
||||
current_choices = [(p.id, p.name_or_id) for p in filtered_ports]
|
||||
self.fields['port_id'].choices = current_choices
|
||||
self.ports = api_fwaas_v2.port_list(request, tenant_id, **kwargs)
|
||||
self.networks = api_fwaas_v2.get_network_names(request)
|
||||
self.routers = api_fwaas_v2.get_router_names(request)
|
||||
self.servers = api_fwaas_v2.get_servers(request)
|
||||
|
||||
self.fields['port_id'].widget.build_columns = self._build_col
|
||||
self.fields['port_id'].choices = self.get_ports(request)
|
||||
|
||||
def get_ports(self, request):
|
||||
return []
|
||||
|
||||
def _build_col(self, option):
|
||||
port = self.ports[option[0]]
|
||||
columns = self._build_option(port)
|
||||
return columns
|
||||
|
||||
def _build_option(self, port):
|
||||
network = self.networks.get(port.network_id)
|
||||
|
||||
network_label = network.name_or_id if network else port.network_id
|
||||
owner_label = ''
|
||||
device_label = ''
|
||||
|
||||
if port.device_owner.startswith('network'):
|
||||
owner_label = 'network'
|
||||
router = self.routers.get(port.device_id, None)
|
||||
device_label = router.name_or_id if router else port.device_id
|
||||
elif port.device_owner.startswith('compute'):
|
||||
owner_label = 'compute'
|
||||
server = self.servers.get(port.device_id, None)
|
||||
device_label = server.name_or_id if server else port.device_id
|
||||
|
||||
columns = (port.name_or_id, network_label, owner_label, device_label)
|
||||
|
||||
# The return value works off of the original themeable select widget
|
||||
# This needs to be maintained for the original javascript to work
|
||||
return columns
|
||||
|
||||
|
||||
class AddPort(PortSelectionForm):
|
||||
failure_url = 'horizon:project:firewalls_v2:index'
|
||||
|
||||
def get_ports(self, request):
|
||||
used_ports = api_fwaas_v2.fwg_port_list(request)
|
||||
ports = self.ports.values()
|
||||
return [(p.id, p.id) for p in ports if p.id not in used_ports]
|
||||
|
||||
def handle(self, request, context):
|
||||
firewallgroup_id = self.initial['id']
|
||||
|
@ -249,22 +295,12 @@ class AddPort(forms.SelfHandlingForm):
|
|||
exceptions.handle(request, msg, redirect=redirect)
|
||||
|
||||
|
||||
class RemovePort(forms.SelfHandlingForm):
|
||||
class RemovePort(PortSelectionForm):
|
||||
failure_url = 'horizon:project:firewalls_v2:index'
|
||||
port_id = forms.ThemableChoiceField(
|
||||
label=_("Ports"), required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(RemovePort, self).__init__(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
ports = self.initial['ports']
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to retrieve port list.'))
|
||||
ports = []
|
||||
|
||||
current_choices = [(p, p) for p in ports]
|
||||
self.fields['port_id'].choices = current_choices
|
||||
def get_ports(self, request):
|
||||
ports = self.initial['ports']
|
||||
return [(p, p) for p in ports]
|
||||
|
||||
def handle(self, request, context):
|
||||
firewallgroup_id = self.initial['id']
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
{% load horizon %}
|
||||
|
||||
{% minifyspace %}
|
||||
<div class="themable-select dropdown {% if not stand_alone %} form-control{% endif %}"
|
||||
xmlns="http://www.w3.org/1999/html">
|
||||
<button type="button" class="btn btn-default dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
{% if value %} title="{{ value }}" {% endif %}
|
||||
aria-expanded="false"
|
||||
{% if options|length < 1 %}
|
||||
disabled="true"
|
||||
{% endif %}
|
||||
>
|
||||
<span class="dropdown-title">
|
||||
{% if options|length < 1 %}
|
||||
{{ empty_text }}
|
||||
{% elif initial_value %}
|
||||
{{ initial_value.1 }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="fa fa-caret-down"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu container-fluid dropdown-table">
|
||||
<li class="row dropdown-thead">
|
||||
<div class="col-xs-12">
|
||||
<div class="row dropdown-tr ">
|
||||
{% if alternate_xs %}
|
||||
<div class="visible-xs-block col-xs-12 dropdown-th">{{ summarized_headers }}</div>
|
||||
{% for column in columns %}
|
||||
<div class="hidden-xs col-sm-{{ column_size }} dropdown-th">{{ column }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for column in columns %}
|
||||
<div class="col-xs-{{ column_size }} dropdown-th">{{ column }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% for option in options %}
|
||||
<li data-original-index="{{ forloop.counter0 }}"
|
||||
class="row dropdown-tr"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
>
|
||||
<a data-select-value="{{ option.0 }}"
|
||||
class="col-xs-12"
|
||||
href="#"
|
||||
>
|
||||
<div class="row">
|
||||
{% if alternate_xs %}
|
||||
<div class="visible-xs-block col-xs-12 dropdown-td">
|
||||
{{ option.1 }}
|
||||
</div>
|
||||
{% for column in option.2 %}
|
||||
<div class="hidden-xs col-sm-{{ column_size }} dropdown-td">{{ column }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for column in option.2 %}
|
||||
<div class="col-xs-{{ column_size }} dropdown-td">{{ column }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<select
|
||||
{% if id %}
|
||||
id="{{ id }}"{% endif %}
|
||||
{% if name %}
|
||||
name="{{ name }}"
|
||||
{% endif %}
|
||||
{% for k,v in select_attrs.items %}
|
||||
{% if k != 'class' or 'switch' in v %}
|
||||
{{ k|safe }}="{{ v }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% for option in options %}
|
||||
<option value="{{ option.0 }}"
|
||||
{% if option.0 == value %}
|
||||
selected="selected"
|
||||
{% endif %}
|
||||
{% if option.3 %}
|
||||
{{ option.3|safe }}
|
||||
{% endif %}>
|
||||
{{ option.1 }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endminifyspace %}
|
|
@ -0,0 +1,260 @@
|
|||
# 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 itertools
|
||||
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon.forms import fields
|
||||
|
||||
"""A custom Horizon Forms Select widget that displays select choices as a table
|
||||
|
||||
The widgets is meant as an optional replacement for the existing Horizon
|
||||
ThemableDynamicSelectWidget which it extends and is compatible with.
|
||||
|
||||
|
||||
Columns
|
||||
-------
|
||||
Columns are defined by setting the widgets 'column' attribute, which is
|
||||
expected to be an iterable of strings, each one corresponding to one column and
|
||||
used for that columns heading.
|
||||
|
||||
|
||||
Rows
|
||||
----
|
||||
Each row corresponds to one choice/select option with a defined value in
|
||||
each column.
|
||||
|
||||
The values displayed in each column are derived using the 'build_columns'
|
||||
attribute, which is expected to be a function that:
|
||||
|
||||
- takes a choice tuple of the form (value, label) as defined
|
||||
for the Django SelectField instances as it's only parameter
|
||||
- returns an iterable of Strings which are rendered as column
|
||||
values for the given choice row in the same order as in the
|
||||
iterable
|
||||
|
||||
The default implementation simply uses the provided value and label as separate
|
||||
column values.
|
||||
|
||||
See the default implementation and example bellow for more details.
|
||||
|
||||
|
||||
Condensed values
|
||||
----------------
|
||||
To maintain visual consistency, the currently selected value is displayed in
|
||||
the 'standard' ThemableDynamicSelectWidget HTML setup. To accommodate this, a
|
||||
condensed, single string value is created from the individual columns and
|
||||
displayed in the select box.
|
||||
|
||||
This behavior can be modified by setting the 'condense' attribute. This is
|
||||
expected to be a function that:
|
||||
|
||||
- Takes the column iterable returned by 'build_columns' function
|
||||
- Returns a single string representation of the choice
|
||||
|
||||
By default, the condensed value is created by joining all of the provided
|
||||
columns and joining them using commas as a delimiter.
|
||||
|
||||
See the default implementation and example bellow for more details.
|
||||
|
||||
|
||||
Small screen reactivity
|
||||
-----------------------
|
||||
Support for small screens (< 768px) is turned on by setting the attribute
|
||||
'alternate_xs' to True. When on, a condesned version of the popup table
|
||||
us used for small screens, where a single column is used with the condensed
|
||||
row values used instead of the full table rows.
|
||||
|
||||
The 'condense' function described above is used to construct this table.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
port_id = forms.ThemableDynamicChoiceField(
|
||||
label=_("Ports"),
|
||||
widget=TableSelectWidget(
|
||||
columns=[
|
||||
'ID',
|
||||
'Name'
|
||||
],
|
||||
build_columns=lambda choice: return (choice[1], choice[0]),
|
||||
choices=[
|
||||
('port 1', 'id1'),
|
||||
('port 2', 'id2')
|
||||
],
|
||||
alternate_xs=True,
|
||||
condense=lambda columns: return ",".join(columns)
|
||||
)
|
||||
)
|
||||
|
||||
Produces:
|
||||
|
||||
+------+--------+
|
||||
| ID | Name |
|
||||
+------+--------+
|
||||
| id1 | port 1 |
|
||||
| id2 | port 2 |
|
||||
+------+--------+
|
||||
|
||||
on normal screens and
|
||||
|
||||
+-------------+
|
||||
| ID, Name |
|
||||
+-------------+
|
||||
| id1, port 1 |
|
||||
| id2, port 2 |
|
||||
+-------------+
|
||||
|
||||
on xs screens.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class TableSelectWidget(fields.ThemableDynamicSelectWidget):
|
||||
def __init__(self,
|
||||
attrs=None,
|
||||
columns=None,
|
||||
alternate_xs=False,
|
||||
empty_text=_("No options available"),
|
||||
other_html=None,
|
||||
condense=None,
|
||||
build_columns=None, *args, **kwargs
|
||||
):
|
||||
"""Initializer for TableSelectWidget
|
||||
|
||||
:param attrs: A { attribute: value } dictionary which is attached to
|
||||
the hidden select element; see
|
||||
ThemableDynamicSelectWidget for further information
|
||||
:param columns: An iterable of column headers/names
|
||||
:param alternate_xs: A truth-y value which enables/disables an
|
||||
alternate rendering method for small screens
|
||||
:param empty_text: The text to be displayed in case no options are
|
||||
available
|
||||
:param other_html: A method for adding custom HTML to the hidden option
|
||||
HTML.
|
||||
NOTE: This mimics the behavior of
|
||||
ThemableDynamicSelectWidget and is retained to
|
||||
maintain compatibility with any related, potential
|
||||
functionality
|
||||
:param condense: A function callback that produces a condensed label
|
||||
for each option
|
||||
:param build_columns: A function used to populate the individual
|
||||
columns in the pop up table for each option
|
||||
"""
|
||||
super(TableSelectWidget, self).__init__(attrs, *args, **kwargs)
|
||||
self.columns = columns or [_('Label'), _('Value'), 'Nothing']
|
||||
|
||||
self.alternate_xs = alternate_xs
|
||||
self.empty_text = empty_text
|
||||
|
||||
if other_html:
|
||||
self.other_html = other_html
|
||||
|
||||
if condense:
|
||||
self.condense = condense
|
||||
|
||||
if build_columns:
|
||||
self.build_columns = build_columns
|
||||
|
||||
@staticmethod
|
||||
def build_columns(choice):
|
||||
"""Default column building method
|
||||
|
||||
Overwrite this method when initializing this widget or using
|
||||
self.fields[name].widget.build_columns in a parent form initialization
|
||||
to customize the behavior (see above for details)
|
||||
|
||||
:param choice:
|
||||
:return:
|
||||
"""
|
||||
return choice
|
||||
|
||||
@staticmethod
|
||||
def condense(choice_columns):
|
||||
"""The default condense method
|
||||
|
||||
Overwrite this method when initializing this widget or using
|
||||
self.fields[name].widget.condense in a parent form initialization to
|
||||
customize the behavior (see above for details)
|
||||
|
||||
:param choice_columns:
|
||||
:return:
|
||||
"""
|
||||
return " / ".join([str(c) for c in choice_columns])
|
||||
|
||||
# Implements the parent 'other_html' construction for compatibility reasons
|
||||
# Can be set in initializer to change the behavior as needed
|
||||
def other_html(self, choice):
|
||||
opt_label = choice[1]
|
||||
|
||||
other_html = self.transform_option_html_attrs(opt_label)
|
||||
data_attr_html = self.get_data_attrs(opt_label)
|
||||
|
||||
if data_attr_html:
|
||||
other_html += ' ' + data_attr_html
|
||||
|
||||
return other_html
|
||||
|
||||
def render(self, name, value, attrs=None, choices=None):
|
||||
new_choices = []
|
||||
initial_value = value
|
||||
|
||||
choices = choices or []
|
||||
|
||||
for opt in itertools.chain(self.choices, choices):
|
||||
other_html = self.other_html(opt)
|
||||
choice_columns = self.build_columns(opt)
|
||||
condensed_label = self.condense(choice_columns)
|
||||
|
||||
built_choice = (
|
||||
opt[0], condensed_label, choice_columns, other_html
|
||||
)
|
||||
|
||||
new_choices.append(built_choice)
|
||||
|
||||
# Initial selection
|
||||
if opt[0] == value:
|
||||
initial_value = built_choice
|
||||
|
||||
if not initial_value and new_choices:
|
||||
initial_value = new_choices[0]
|
||||
|
||||
element_id = attrs.pop('id', 'id_%s' % name)
|
||||
|
||||
# Size of individual columns in terms of the bootstrap grid - used
|
||||
# for styling purposes
|
||||
column_size = 12 // len(self.columns)
|
||||
|
||||
# Creates a single string label for all columns for use with small
|
||||
# screens
|
||||
condensed_headers = self.condense(self.columns)
|
||||
|
||||
template = get_template('project/firewalls_v2/table_select.html')
|
||||
|
||||
select_attrs = self.build_attrs(attrs)
|
||||
|
||||
context = {
|
||||
'name': name,
|
||||
'options': new_choices,
|
||||
'id': element_id,
|
||||
'value': value,
|
||||
'initial_value': initial_value,
|
||||
'select_attrs': select_attrs,
|
||||
'column_size': column_size,
|
||||
'columns': self.columns,
|
||||
'condensed_headers': condensed_headers,
|
||||
'alternate_xs': self.alternate_xs,
|
||||
'empty_text': self.empty_text
|
||||
}
|
||||
return template.render(context)
|
|
@ -14,3 +14,67 @@
|
|||
@include common_box_list_selected("router");
|
||||
}
|
||||
}
|
||||
|
||||
// Table like styling of boostrap grid options in a bootstrap-dropdown
|
||||
// compatible with the horizon dropdown javascript
|
||||
.dropdown-table {
|
||||
@media only screen and (min-width: 768px) {
|
||||
min-width: $modal-md - $grid-gutter-width;
|
||||
}
|
||||
|
||||
margin-bottom: $line-height-computed;
|
||||
background-color: $body-bg;
|
||||
padding: 10px;
|
||||
|
||||
.dropdown-thead {
|
||||
margin: 0px;
|
||||
|
||||
.col-xs-12 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown-tr {
|
||||
.dropdown-th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 1px solid $table-border-color;
|
||||
border-top: 0;
|
||||
|
||||
display: table-cell;
|
||||
vertical-align: inherit;
|
||||
font-weight: bold;
|
||||
text-align: -internal-center;
|
||||
|
||||
color: $dropdown-header-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-tr {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
|
||||
.col-xs-12 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown-th, .dropdown-td {
|
||||
padding: $table-cell-padding;
|
||||
line-height: $line-height-base;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid $table-border-color;
|
||||
color: $dropdown-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-options {
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import mock
|
||||
from neutronclient.v2_0.client import Client as neutronclient
|
||||
import openstack_dashboard.api.nova as nova
|
||||
|
||||
from openstack_dashboard.test import helpers
|
||||
|
||||
|
@ -23,6 +24,142 @@ from neutron_fwaas_dashboard.test import helpers as test
|
|||
|
||||
class FwaasV2ApiTests(test.APITestCase):
|
||||
|
||||
@helpers.create_mocks({nova: ('server_list',)})
|
||||
def test_get_servers(self):
|
||||
fields = ['id', 'name']
|
||||
|
||||
mock_servers = {
|
||||
'916562da-fa95-4ae1-8bea-0b45f2f8297a': self._mock_server(
|
||||
id='916562da-fa95-4ae1-8bea-0b45f2f8297a',
|
||||
name='mock-server-1'
|
||||
),
|
||||
'7038e456-3067-493a-8f2b-69bc26acbccf': self._mock_server(
|
||||
id='7038e456-3067-493a-8f2b-69bc26acbccf',
|
||||
name='mock-server-2'
|
||||
),
|
||||
'23f683e5-8536-4e5a-806b-0382b02743dc': self._mock_server(
|
||||
id='23f683e5-8536-4e5a-806b-0382b02743dc',
|
||||
name='mock-server-3'
|
||||
)
|
||||
}
|
||||
|
||||
mock_server_ids = sorted(mock_servers.keys())
|
||||
|
||||
self.mock_server_list.return_value = [list(mock_servers.values())]
|
||||
|
||||
servers = api_fwaas_v2.get_servers(self.request)
|
||||
|
||||
server_ids = sorted(servers.keys())
|
||||
|
||||
self.assertEqual(server_ids, mock_server_ids)
|
||||
for key in mock_server_ids:
|
||||
expected_server = mock_servers[key]
|
||||
server = servers[key]
|
||||
self._assert_subobject(expected_server, server, fields)
|
||||
|
||||
def _assert_subobject(self, child, parent, fields):
|
||||
for field in fields:
|
||||
self.assertEqual(
|
||||
getattr(child, field),
|
||||
getattr(parent, field)
|
||||
)
|
||||
|
||||
def _mock_server(self, **kwargs):
|
||||
server = nova.Server({}, self.request)
|
||||
for key, val in kwargs.items():
|
||||
setattr(server, key, val)
|
||||
return server
|
||||
|
||||
@helpers.create_mocks({neutronclient: ('list_networks',)})
|
||||
def test_get_networks(self):
|
||||
fields = ['name', 'id']
|
||||
|
||||
mock_networks = {
|
||||
'64e8c993-1c99-40fb-a8bc-42d3fd487a97': {
|
||||
'name': 'mock-network-1',
|
||||
'id': '64e8c993-1c99-40fb-a8bc-42d3fd487a97'
|
||||
},
|
||||
'f1bd4bb5-2bf3-4e0e-9c8d-9a1a500eaece': {
|
||||
'name': 'mock-network-2',
|
||||
'id': 'f1bd4bb5-2bf3-4e0e-9c8d-9a1a500eaece'
|
||||
},
|
||||
'74173cf1-461e-4fd0-881e-2a0cc4a94e14': {
|
||||
'name': 'mock-network-3',
|
||||
'id': '74173cf1-461e-4fd0-881e-2a0cc4a94e14'
|
||||
}
|
||||
}
|
||||
mock_network_ids = sorted(mock_networks.keys())
|
||||
|
||||
self.mock_list_networks.return_value = {
|
||||
'networks': list(mock_networks.values())
|
||||
}
|
||||
|
||||
network_names = api_fwaas_v2.get_network_names(self.request)
|
||||
|
||||
self.mock_list_networks.assert_called_once_with(fields=fields)
|
||||
|
||||
network_ids = sorted(network_names.keys())
|
||||
|
||||
self.assertEqual(network_ids, mock_network_ids)
|
||||
|
||||
for key in mock_network_ids:
|
||||
self._assert_api_dict(
|
||||
network_names[key]._apidict,
|
||||
mock_networks[key],
|
||||
fields
|
||||
)
|
||||
|
||||
@helpers.create_mocks({neutronclient: ('list_routers',)})
|
||||
def test_get_router_names(self):
|
||||
fields = ['name', 'id']
|
||||
|
||||
mock_routers = {
|
||||
'9d143b82-bd74-4ccf-81ba-9b7e02f3f7b2': {
|
||||
'name': 'mock-router-1',
|
||||
'id': '9d143b82-bd74-4ccf-81ba-9b7e02f3f7b2'
|
||||
},
|
||||
'84d72522-1c26-4d28-83ed-b8653ac5d38c': {
|
||||
'name': 'mock-router-2',
|
||||
'id': '84d72522-1c26-4d28-83ed-b8653ac5d38c'
|
||||
},
|
||||
'2149de19-840a-4b41-8a44-4755ce8a881b': {
|
||||
'name': 'mock-router-3',
|
||||
'id': '2149de19-840a-4b41-8a44-4755ce8a881b'
|
||||
}
|
||||
}
|
||||
mock_router_ids = sorted(mock_routers.keys())
|
||||
|
||||
# Mock API call
|
||||
self.mock_list_routers.return_value = {
|
||||
'routers': list(mock_routers.values())
|
||||
}
|
||||
# call results
|
||||
router_names = api_fwaas_v2.get_router_names(self.request)
|
||||
|
||||
# Check that the correct filters were applied for the API call
|
||||
self.mock_list_routers.assert_called_once_with(fields=fields)
|
||||
# Ensure that exactly the expected mock data ids have been retrieved
|
||||
router_ids = sorted(router_names.keys())
|
||||
self.assertEqual(router_ids, mock_router_ids)
|
||||
|
||||
# Check that the returned values correspond to the (mocked) API data
|
||||
for key in mock_router_ids:
|
||||
# Note that _apidict is being checked
|
||||
self._assert_api_dict(
|
||||
router_names[key]._apidict,
|
||||
mock_routers[key],
|
||||
fields
|
||||
)
|
||||
|
||||
def _assert_api_dict(self, actual, expected, fields):
|
||||
# Ensure exactly the required fields have been retrieved
|
||||
actual_fields = sorted(actual.keys())
|
||||
self.assertEqual(actual_fields, sorted(fields))
|
||||
|
||||
# Ensure expected datum was returned in each field
|
||||
for field in fields:
|
||||
self.assertEqual(actual[field], expected[field])
|
||||
|
||||
@helpers.create_mocks({neutronclient: ('create_fwaas_firewall_rule',)})
|
||||
def test_rule_create(self):
|
||||
rule1 = self.fw_rules_v2.first()
|
||||
|
@ -418,6 +555,26 @@ class FwaasV2ApiTests(test.APITestCase):
|
|||
mock.call(shared=True),
|
||||
])
|
||||
|
||||
@helpers.create_mocks({neutronclient: ('list_fwaas_firewall_groups', )})
|
||||
def test_fwg_port_list(self):
|
||||
mock_port_id_1 = '62b974c5-48fb-4fd1-946f-5ace1d970dd4'
|
||||
mock_port_id_2 = 'da012bb6-c350-4a72-b6c9-69c4f2008aa4'
|
||||
mock_port_id_3 = 'c2a2ce11-71dd-49a5-84ec-2407ecb42106'
|
||||
|
||||
mock_groups = [
|
||||
{'ports': [mock_port_id_1, mock_port_id_2]},
|
||||
{'ports': []},
|
||||
{'ports': [mock_port_id_3]}
|
||||
]
|
||||
self.mock_list_fwaas_firewall_groups.return_value = {
|
||||
'firewall_groups': mock_groups
|
||||
}
|
||||
|
||||
expected_set = {mock_port_id_1, mock_port_id_2, mock_port_id_3}
|
||||
retrieved_set = api_fwaas_v2.fwg_port_list(self.request)
|
||||
|
||||
self.assertEqual(expected_set, retrieved_set)
|
||||
|
||||
@helpers.create_mocks({neutronclient: ('list_ports',
|
||||
'list_fwaas_firewall_groups')})
|
||||
def test_fwg_port_list_for_tenant(self):
|
||||
|
|
Loading…
Reference in New Issue