diff --git a/openstack_dashboard/api/__init__.py b/openstack_dashboard/api/__init__.py index 5c9705498..06e12ad68 100644 --- a/openstack_dashboard/api/__init__.py +++ b/openstack_dashboard/api/__init__.py @@ -5,6 +5,7 @@ # All Rights Reserved. # # Copyright 2012 Nebula, Inc. +# Copyright 2013 Big Switch Networks # # 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 @@ -32,11 +33,12 @@ In other words, Horizon developers not working on openstack_dashboard.api shouldn't need to understand the finer details of APIs for Keystone/Nova/Glance/Swift et. al. """ -import base -import cinder -import glance -import keystone -import network -import nova -import quantum -import swift +from openstack_dashboard.api import base +from openstack_dashboard.api import cinder +from openstack_dashboard.api import glance +from openstack_dashboard.api import keystone +from openstack_dashboard.api import network +from openstack_dashboard.api import nova +from openstack_dashboard.api import quantum +from openstack_dashboard.api import lbaas +from openstack_dashboard.api import swift diff --git a/openstack_dashboard/api/lbaas.py b/openstack_dashboard/api/lbaas.py new file mode 100644 index 000000000..550371948 --- /dev/null +++ b/openstack_dashboard/api/lbaas.py @@ -0,0 +1,291 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 __future__ import absolute_import + +from openstack_dashboard.api.quantum import QuantumAPIDictWrapper +from openstack_dashboard.api.quantum import quantumclient +from openstack_dashboard.api.quantum import subnet_get + + +class Vip(QuantumAPIDictWrapper): + """Wrapper for quantum load balancer vip""" + + def __init__(self, apiresource): + super(Vip, self).__init__(apiresource) + + +class Pool(QuantumAPIDictWrapper): + """Wrapper for quantum load balancer pool""" + + def __init__(self, apiresource): + super(Pool, self).__init__(apiresource) + + class AttributeDict(dict): + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + def readable(self, request): + pFormatted = {'id': self.id, + 'name': self.name, + 'description': self.description, + 'protocol': self.protocol} + try: + pFormatted['subnet_id'] = self.subnet_id + pFormatted['subnet_name'] = subnet_get( + request, self.subnet_id).cidr + except: + pFormatted['subnet_id'] = self.subnet_id + pFormatted['subnet_name'] = self.subnet_id + + if self.vip_id is not None: + try: + pFormatted['vip_id'] = self.vip_id + pFormatted['vip_name'] = vip_get( + request, self.vip_id).name + except: + pFormatted['vip_id'] = self.vip_id + pFormatted['vip_name'] = self.vip_id + else: + pFormatted['vip_id'] = None + pFormatted['vip_name'] = None + + return self.AttributeDict(pFormatted) + + +class Member(QuantumAPIDictWrapper): + """Wrapper for quantum load balancer member""" + + def __init__(self, apiresource): + super(Member, self).__init__(apiresource) + + class AttributeDict(dict): + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + def readable(self, request): + mFormatted = {'id': self.id, + 'address': self.address, + 'protocol_port': self.protocol_port} + try: + mFormatted['pool_id'] = self.pool_id + mFormatted['pool_name'] = pool_get( + request, self.pool_id).name + except: + mFormatted['pool_id'] = self.pool_id + mFormatted['pool_name'] = self.pool_id + + return self.AttributeDict(mFormatted) + + +class PoolStats(QuantumAPIDictWrapper): + """Wrapper for quantum load balancer pool stats""" + + def __init__(self, apiresource): + super(PoolStats, self).__init__(apiresource) + + +class PoolMonitor(QuantumAPIDictWrapper): + """Wrapper for quantum load balancer pool health monitor""" + + def __init__(self, apiresource): + super(PoolMonitor, self).__init__(apiresource) + + +def vip_create(request, **kwargs): + """Create a vip for a specified pool. + + :param request: request context + :param address: virtual IP address + :param name: name for vip + :param description: description for vip + :param subnet_id: subnet_id for subnet of vip + :param protocol_port: transport layer port number for vip + :returns: Vip object + """ + body = {'vip': {'address': kwargs['address'], + 'name': kwargs['name'], + 'description': kwargs['description'], + 'subnet_id': kwargs['subnet_id'], + 'protocol_port': kwargs['protocol_port'], + 'protocol': kwargs['protocol'], + 'pool_id': kwargs['pool_id'], + 'session_persistence': kwargs['session_persistence'], + 'connection_limit': kwargs['connection_limit'], + 'admin_state_up': kwargs['admin_state_up'] + }} + vip = quantumclient(request).create_vip(body).get('vip') + return Vip(vip) + + +def vips_get(request, **kwargs): + vips = quantumclient(request).list_vips().get('vips') + return [Vip(v) for v in vips] + + +def vip_get(request, vip_id): + vip = quantumclient(request).show_vip(vip_id).get('vip') + return Vip(vip) + + +# not linked to UI yet +def vip_update(request, vip_id, **kwargs): + vip = quantumclient(request).update_vip(vip_id, kwargs).get('vip') + return Vip(vip) + + +def vip_delete(request, vip_id): + quantumclient(request).delete_vip(vip_id) + + +def pool_create(request, **kwargs): + """Create a pool for specified protocol + + :param request: request context + :param name: name for pool + :param description: description for pool + :param subnet_id: subnet_id for subnet of pool + :param protocol: load balanced protocol + :param lb_method: load balancer method + :param admin_state_up: admin state (default on) + """ + body = {'pool': {'name': kwargs['name'], + 'description': kwargs['description'], + 'subnet_id': kwargs['subnet_id'], + 'protocol': kwargs['protocol'], + 'lb_method': kwargs['lb_method'], + 'admin_state_up': kwargs['admin_state_up'] + }} + pool = quantumclient(request).create_pool(body).get('pool') + return Pool(pool) + + +def pools_get(request, **kwargs): + pools = quantumclient(request).list_pools().get('pools') + return [Pool(p) for p in pools] + + +def pool_get(request, pool_id): + pool = quantumclient(request).show_pool(pool_id).get('pool') + return Pool(pool) + + +def pool_update(request, pool_id, **kwargs): + pool = quantumclient(request).update_pool(pool_id, kwargs).get('pool') + return Pool(pool) + + +def pool_delete(request, pool): + quantumclient(request).delete_pool(pool) + + +# not linked to UI yet +def pool_stats(request, pool_id, **kwargs): + stats = quantumclient(request).retrieve_pool_stats(pool_id, **kwargs) + return PoolStats(stats) + + +def pool_health_monitor_create(request, **kwargs): + """Create a health monitor and associate with pool + + :param request: request context + :param type: type of monitor + :param delay: delay of monitor + :param timeout: timeout of monitor + :param max_retries: max retries [1..10] + :param http_method: http method + :param url_path: url path + :param expected_codes: http return code + :param admin_state_up: admin state + """ + body = {'health_monitor': {'type': kwargs['type'], + 'delay': kwargs['delay'], + 'timeout': kwargs['timeout'], + 'max_retries': kwargs['max_retries'], + 'http_method': kwargs['http_method'], + 'url_path': kwargs['url_path'], + 'expected_codes': kwargs['expected_codes'], + 'admin_state_up': kwargs['admin_state_up'] + }} + mon = quantumclient(request).create_health_monitor(body).get( + 'health_monitor') + body = {'health_monitor': {'id': mon['id']}} + quantumclient(request).associate_health_monitor( + kwargs['pool_id'], body) + return PoolMonitor(mon) + + +def pool_health_monitors_get(request, **kwargs): + monitors = quantumclient(request + ).list_health_monitors().get('health_monitors') + return [PoolMonitor(m) for m in monitors] + + +def pool_health_monitor_get(request, monitor_id): + monitor = quantumclient(request + ).show_health_monitor(monitor_id + ).get('health_monitor') + return PoolMonitor(monitor) + + +def pool_health_monitor_delete(request, mon_id): + quantumclient(request).delete_health_monitor(mon_id) + + +def member_create(request, **kwargs): + """Create a load balance member + + :param request: request context + :param pool_id: pool_id of pool for member + :param address: IP address + :param protocol_port: transport layer port number + :param weight: weight for member + :param admin_state_up: admin_state + """ + body = {'member': {'pool_id': kwargs['pool_id'], + 'address': kwargs['address'], + 'protocol_port': kwargs['protocol_port'], + 'weight': kwargs['weight'], + 'admin_state_up': kwargs['admin_state_up'] + }} + member = quantumclient(request).create_member(body).get('member') + return Member(member) + + +def members_get(request, **kwargs): + members = quantumclient(request).list_members().get('members') + return [Member(m) for m in members] + + +def member_get(request, member_id): + member = quantumclient(request).show_member(member_id).get('member') + return Member(member) + + +# not linked to UI yet +def member_update(request, member_id, **kwargs): + member = quantumclient(request).update_member(member_id, kwargs) + return Member(member) + + +def member_delete(request, mem_id): + quantumclient(request).delete_member(mem_id) diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py index 419b0e66c..0ddbf0d28 100644 --- a/openstack_dashboard/dashboards/project/dashboard.py +++ b/openstack_dashboard/dashboards/project/dashboard.py @@ -29,7 +29,8 @@ class BasePanels(horizon.PanelGroup): 'access_and_security', 'networks', 'routers', - 'network_topology') + 'network_topology', + 'loadbalancers') class ObjectStorePanels(horizon.PanelGroup): diff --git a/openstack_dashboard/dashboards/project/loadbalancers/__init__.py b/openstack_dashboard/dashboards/project/loadbalancers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/project/loadbalancers/models.py b/openstack_dashboard/dashboards/project/loadbalancers/models.py new file mode 100644 index 000000000..1b3d5f9ef --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/openstack_dashboard/dashboards/project/loadbalancers/panel.py b/openstack_dashboard/dashboards/project/loadbalancers/panel.py new file mode 100644 index 000000000..b59d34043 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/panel.py @@ -0,0 +1,16 @@ +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +import horizon + +from openstack_dashboard.dashboards.project import dashboard + + +class LoadBalancer(horizon.Panel): + name = _("Load Balancers") + slug = "loadbalancers" + permissions = ('openstack.services.network',) + +if hasattr(settings, 'OPENSTACK_QUANTUM_NETWORK'): + if getattr(settings, 'OPENSTACK_QUANTUM_NETWORK')['enable_lb']: + dashboard.Project.register(LoadBalancer) diff --git a/openstack_dashboard/dashboards/project/loadbalancers/tables.py b/openstack_dashboard/dashboards/project/loadbalancers/tables.py new file mode 100644 index 000000000..91805b40b --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/tables.py @@ -0,0 +1,162 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 logging + +from django.utils import http +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from horizon import tables +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class AddPoolLink(tables.LinkAction): + name = "addpool" + verbose_name = _("Add Pool") + url = "horizon:project:loadbalancers:addpool" + classes = ("btn-addpool",) + + +class AddVipLink(tables.LinkAction): + name = "addvip" + verbose_name = _("Add Vip") + classes = ("btn-addvip",) + + def get_link_url(self, pool): + base_url = reverse("horizon:project:loadbalancers:addvip", + kwargs={'pool_id': pool.id}) + return base_url + + def allowed(self, request, datum=None): + if datum and datum.vip_id: + return False + return True + + +class AddMemberLink(tables.LinkAction): + name = "addmember" + verbose_name = _("Add Member") + url = "horizon:project:loadbalancers:addmember" + classes = ("btn-addmember",) + + +class AddMonitorLink(tables.LinkAction): + name = "addmonitor" + verbose_name = _("Add Monitor") + url = "horizon:project:loadbalancers:addmonitor" + classes = ("btn-addmonitor",) + + +class DeleteVipLink(tables.DeleteAction): + name = "deletevip" + action_present = _("Delete") + action_past = _("Scheduled deletion of") + data_type_singular = _("Vip") + data_type_plural = _("Vips") + + def allowed(self, request, datum=None): + if datum and not datum.vip_id: + return False + return True + + +class DeletePoolLink(tables.DeleteAction): + name = "deletepool" + action_present = _("Delete") + action_past = _("Scheduled deletion of") + data_type_singular = _("Pool") + data_type_plural = _("Pools") + + +class DeleteMonitorLink(tables.DeleteAction): + name = "deletemonitor" + action_present = _("Delete") + action_past = _("Scheduled deletion of") + data_type_singular = _("Monitor") + data_type_plural = _("Monitors") + + +class DeleteMemberLink(tables.DeleteAction): + name = "deletemember" + action_present = _("Delete") + action_past = _("Scheduled deletion of") + data_type_singular = _("Member") + data_type_plural = _("Members") + + +def get_vip_link(pool): + return reverse("horizon:project:loadbalancers:vipdetails", + args=(http.urlquote(pool.vip_id),)) + + +class PoolsTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:project:loadbalancers:pooldetails") + description = tables.Column('description', verbose_name=_("Description")) + subnet_name = tables.Column('subnet_name', verbose_name=_("Subnet")) + protocol = tables.Column('protocol', verbose_name=_("Protocol")) + vip_name = tables.Column('vip_name', verbose_name=_("VIP"), + link=get_vip_link) + + class Meta: + name = "poolstable" + verbose_name = _("Pools") + table_actions = (AddPoolLink, DeletePoolLink) + row_actions = (AddVipLink, DeleteVipLink, DeletePoolLink) + + +def get_pool_link(member): + return reverse("horizon:project:loadbalancers:pooldetails", + args=(http.urlquote(member.pool_id),)) + + +def get_member_link(member): + return reverse("horizon:project:loadbalancers:memberdetails", + args=(http.urlquote(member.id),)) + + +class MembersTable(tables.DataTable): + address = tables.Column('address', + verbose_name=_("IP Address"), + link=get_member_link) + protocol_port = tables.Column('protocol_port', + verbose_name=_("Protocol Port")) + pool_name = tables.Column("pool_name", + verbose_name=_("Pool"), link=get_pool_link) + + class Meta: + name = "memberstable" + verbose_name = _("Members") + table_actions = (AddMemberLink, DeleteMemberLink) + row_actions = (DeleteMemberLink,) + + +class MonitorsTable(tables.DataTable): + id = tables.Column("id", + verbose_name=_("ID"), + link="horizon:project:loadbalancers:monitordetails") + monitorType = tables.Column('type', verbose_name=_("Monitor Type")) + + class Meta: + name = "monitorstable" + verbose_name = _("Monitors") + table_actions = (AddMonitorLink, DeleteMonitorLink) + row_actions = (DeleteMonitorLink,) diff --git a/openstack_dashboard/dashboards/project/loadbalancers/tabs.py b/openstack_dashboard/dashboards/project/loadbalancers/tabs.py new file mode 100644 index 000000000..97631bc7d --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/tabs.py @@ -0,0 +1,170 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 re + +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import tabs +from horizon import tables + +from openstack_dashboard import api + +from .tables import PoolsTable, MembersTable, MonitorsTable + + +class PoolsTab(tabs.TableTab): + table_classes = (PoolsTable,) + name = _("Pools") + slug = "pools" + template_name = "horizon/common/_detail_table.html" + + def get_poolstable_data(self): + try: + pools = api.lbaas.pools_get(self.tab_group.request) + poolsFormatted = [p.readable(self.tab_group.request) for + p in pools] + except: + poolsFormatted = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve pools list.')) + return poolsFormatted + + +class MembersTab(tabs.TableTab): + table_classes = (MembersTable,) + name = _("Members") + slug = "members" + template_name = "horizon/common/_detail_table.html" + + def get_memberstable_data(self): + try: + members = api.lbaas.members_get(self.tab_group.request) + membersFormatted = [m.readable(self.tab_group.request) for + m in members] + except: + membersFormatted = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve member list.')) + return membersFormatted + + +class MonitorsTab(tabs.TableTab): + table_classes = (MonitorsTable,) + name = _("Monitors") + slug = "monitors" + template_name = "horizon/common/_detail_table.html" + + def get_monitorstable_data(self): + try: + monitors = api.lbaas.pool_health_monitors_get( + self.tab_group.request) + except: + monitors = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve monitor list.')) + return monitors + + +class LoadBalancerTabs(tabs.TabGroup): + slug = "lbtabs" + tabs = (PoolsTab, MembersTab, MonitorsTab) + sticky = True + + +class PoolDetailsTab(tabs.Tab): + name = _("Pool Details") + slug = "pooldetails" + template_name = "project/loadbalancers/_pool_details.html" + + def get_context_data(self, request): + pid = self.tab_group.kwargs['pool_id'] + try: + pool = api.lbaas.pool_get(request, pid) + except: + pool = [] + exceptions.handle(request, + _('Unable to retrieve pool details.')) + return {'pool': pool} + + +class VipDetailsTab(tabs.Tab): + name = _("Vip Details") + slug = "vipdetails" + template_name = "project/loadbalancers/_vip_details.html" + + def get_context_data(self, request): + vid = self.tab_group.kwargs['vip_id'] + try: + vip = api.lbaas.vip_get(request, vid) + except: + vip = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve vip details.')) + return {'vip': vip} + + +class MemberDetailsTab(tabs.Tab): + name = _("Member Details") + slug = "memberdetails" + template_name = "project/loadbalancers/_member_details.html" + + def get_context_data(self, request): + mid = self.tab_group.kwargs['member_id'] + try: + member = api.lbaas.member_get(request, mid) + except: + member = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve member details.')) + return {'member': member} + + +class MonitorDetailsTab(tabs.Tab): + name = _("Monitor Details") + slug = "monitordetails" + template_name = "project/loadbalancers/_monitor_details.html" + + def get_context_data(self, request): + mid = self.tab_group.kwargs['monitor_id'] + try: + monitor = api.lbaas.pool_health_monitor_get(request, mid) + except: + monitor = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve monitor details.')) + return {'monitor': monitor} + + +class PoolDetailsTabs(tabs.TabGroup): + slug = "pooltabs" + tabs = (PoolDetailsTab,) + + +class VipDetailsTabs(tabs.TabGroup): + slug = "viptabs" + tabs = (VipDetailsTab,) + + +class MemberDetailsTabs(tabs.TabGroup): + slug = "membertabs" + tabs = (MemberDetailsTab,) + + +class MonitorDetailsTabs(tabs.TabGroup): + slug = "monitortabs" + tabs = (MonitorDetailsTab,) diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_member_details.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_member_details.html new file mode 100644 index 000000000..68b120677 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_member_details.html @@ -0,0 +1,30 @@ +{% load i18n sizeformat parse_date %} + +
+
+
+
{% trans "ID: " %}
+
{{ member.id }}
+ +
{% trans "Tenant ID: " %}
+
{{ member.tenant_id }}
+ +
{% trans "Pool ID: " %}
+
{{ member.pool_id }}
+ +
{% trans "Address: " %}
+
{{ member.address }}
+ +
{% trans "Protocol Port: " %}
+
{{ member.protocol_port }}
+ +
{% trans "Weight: " %}
+
{{ member.weight }}
+ +
{% trans "Admin State Up: " %}
+
{{ member.admin_state_up }}
+ +
{% trans "Status: " %}
+
{{ member.status }}
+
+
diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_members_tab.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_members_tab.html new file mode 100644 index 000000000..7580cda44 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_members_tab.html @@ -0,0 +1,5 @@ +{% block main %} + + {{ table.render }} + +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitor_details.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitor_details.html new file mode 100644 index 000000000..e8c7bbdb8 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitor_details.html @@ -0,0 +1,39 @@ +{% load i18n sizeformat parse_date %} + +
+
+
+
{% trans "ID: " %}
+
{{ monitor.id }}
+ +
{% trans "Tenant ID: " %}
+
{{ monitor.tenant_id }}
+ +
{% trans "Type: " %}
+
{{ monitor.type }}
+ +
{% trans "Delay: " %}
+
{{ monitor.delay }}
+ +
{% trans "Timeout: " %}
+
{{ monitor.timeout }}
+ +
{% trans "Max Retries: " %}
+
{{ monitor.max_retries }}
+ +
{% trans "HTTP Method: " %}
+
{{ monitor.http_method }}
+ +
{% trans "URL Path: " %}
+
{{ monitor.url_path }}
+ +
{% trans "Expected Codes: " %}
+
{{ monitor.expected_codes }}
+ +
{% trans "Admin State Up: " %}
+
{{ monitor.admin_state_up }}
+ +
{% trans "Status: " %}
+
{{ monitor.status }}
+
+
diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitors_tab.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitors_tab.html new file mode 100644 index 000000000..7580cda44 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitors_tab.html @@ -0,0 +1,5 @@ +{% block main %} + + {{ table.render }} + +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pool_details.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pool_details.html new file mode 100644 index 000000000..b8918abfa --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pool_details.html @@ -0,0 +1,42 @@ +{% load i18n sizeformat parse_date %} + +
+
+
+
{% trans "ID: " %}
+
{{ pool.id }}
+ +
{% trans "Tenant ID: " %}
+
{{ pool.tenant_id }}
+ +
{% trans "VIP ID: " %}
+
{{ pool.vip_id }}
+ +
{% trans "Name: " %}
+
{{ pool.name }}
+ +
{% trans "Description: " %}
+
{{ pool.description }}
+ +
{% trans "Subnet ID: " %}
+
{{ pool.subnet_id }}
+ +
{% trans "Protocol: " %}
+
{{ pool.protocol }}
+ +
{% trans "Load Balancing Method: " %}
+
{{ pool.lb_method }}
+ +
{% trans "Members: " %}
+
{{ pool.members }}
+ +
{% trans "Health Monitors: " %}
+
{{ pool.health_monitors }}
+ +
{% trans "Admin State Up: " %}
+
{{ pool.admin_state_up }}
+ +
{% trans "Status: " %}
+
{{ pool.status }}
+
+
diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pools_tab.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pools_tab.html new file mode 100644 index 000000000..da23734d5 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pools_tab.html @@ -0,0 +1,5 @@ +{% block main %} + + {{ poolstable.render }} + +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_vip_details.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_vip_details.html new file mode 100644 index 000000000..e56b3deec --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_vip_details.html @@ -0,0 +1,48 @@ +{% load i18n sizeformat parse_date %} + +
+
+
+
{% trans "ID: " %}
+
{{ vip.id }}
+ +
{% trans "Tenant ID: " %}
+
{{ vip.tenant_id }}
+ +
{% trans "Name: " %}
+
{{ vip.name }}
+ +
{% trans "Description: " %}
+
{{ vip.description }}
+ +
{% trans "Subnet ID: " %}
+
{{ vip.subnet_id }}
+ +
{% trans "Address: " %}
+
{{ vip.address }}
+ +
{% trans "Protocol Port: " %}
+
{{ vip.protocol_port }}
+ +
{% trans "Protocol: " %}
+
{{ vip.protocol }}
+ +
{% trans "Pool ID: " %}
+
{{ vip.pool_id }}
+ +
{% trans "Session Persistence: " %}
+
{% trans "Type: " %}{{ vip.session_persistence.type }}
+ {% if vip.session_persistence.cookie_name %} +
{% trans "Cookie Name: " %}{{ vip.session_persistence.cookie_name }}
+ {% endif %} + +
{% trans "Connection Limit: " %}
+
{{ vip.connection_limit }}
+ +
{% trans "Admin State Up: " %}
+
{{ vip.admin_state_up }}
+ +
{% trans "Status: " %}
+
{{ vip.status }}
+
+
diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmember.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmember.html new file mode 100644 index 000000000..c60bf54b8 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmember.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Member" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add New Member") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmonitor.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmonitor.html new file mode 100644 index 000000000..92d2cb991 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmonitor.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Monitor" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add New Monitor") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addpool.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addpool.html new file mode 100644 index 000000000..6a75ee7c6 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addpool.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Pool" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Add New Pool") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addvip.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addvip.html new file mode 100644 index 000000000..1492e74e7 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addvip.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Specify Vip" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Specify Vip") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/details_tabs.html b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/details_tabs.html new file mode 100644 index 000000000..17ee96ca2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/details_tabs.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Load Balancer" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Load Balancer") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/loadbalancers/tests.py b/openstack_dashboard/dashboards/project/loadbalancers/tests.py new file mode 100644 index 000000000..8a1510753 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/tests.py @@ -0,0 +1,299 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import json + +from mox import IsA +from django import http +from django.core.urlresolvers import reverse + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test +from openstack_dashboard.api.lbaas import Pool, Vip, Member, PoolMonitor + +from .tabs import LoadBalancerTabs, MembersTab, PoolsTab, MonitorsTab +from .workflows import AddPool, AddMember, AddMonitor, AddVip + + +class LoadBalancerTests(test.TestCase): + class AttributeDict(dict): + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + DASHBOARD = 'project' + INDEX_URL = reverse('horizon:%s:loadbalancers:index' % DASHBOARD) + + ADDPOOL_PATH = 'horizon:%s:loadbalancers:addpool' % DASHBOARD + ADDVIP_PATH = 'horizon:%s:loadbalancers:addvip' % DASHBOARD + ADDMEMBER_PATH = 'horizon:%s:loadbalancers:addmember' % DASHBOARD + ADDMONITOR_PATH = 'horizon:%s:loadbalancers:addmonitor' % DASHBOARD + + POOL_DETAIL_PATH = 'horizon:%s:loadbalancers:pooldetails' % DASHBOARD + VIP_DETAIL_PATH = 'horizon:%s:loadbalancers:vipdetails' % DASHBOARD + MEMBER_DETAIL_PATH = 'horizon:%s:loadbalancers:memberdetails' % DASHBOARD + MONITOR_DETAIL_PATH = 'horizon:%s:loadbalancers:monitordetails' % DASHBOARD + + def set_up_expect(self): + # retrieve pools + subnet = self.subnets.first() + + vip1 = self.vips.first() + + vip2 = self.vips.list()[1] + + api.lbaas.pools_get( + IsA(http.HttpRequest)).AndReturn(self.pools.list()) + + api.lbaas.vip_get(IsA(http.HttpRequest), vip1.id).AndReturn(vip1) + api.lbaas.vip_get(IsA(http.HttpRequest), vip2.id).AndReturn(vip2) + + # retrieves members + api.lbaas.members_get( + IsA(http.HttpRequest)).AndReturn(self.members.list()) + + pool1 = self.pools.first() + pool2 = self.pools.list()[1] + + api.lbaas.pool_get(IsA(http.HttpRequest), + self.members.list()[0].pool_id).AndReturn(pool1) + api.lbaas.pool_get(IsA(http.HttpRequest), + self.members.list()[1].pool_id).AndReturn(pool2) + + # retrieves monitors + api.lbaas.pool_health_monitors_get( + IsA(http.HttpRequest)).AndReturn(self.monitors.list()) + + def set_up_expect_with_exception(self): + api.lbaas.pools_get( + IsA(http.HttpRequest)).AndRaise(self.exceptions.quantum) + api.lbaas.members_get( + IsA(http.HttpRequest)).AndRaise(self.exceptions.quantum) + api.lbaas.pool_health_monitors_get( + IsA(http.HttpRequest)).AndRaise(self.exceptions.quantum) + + @test.create_stubs({api.lbaas: ('pools_get', 'vip_get', + 'members_get', 'pool_get', + 'pool_health_monitors_get'), + api.quantum: ('subnet_get',)}) + def test_index_pools(self): + self.set_up_expect() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['table'].data), + len(self.pools.list())) + + @test.create_stubs({api.lbaas: ('pools_get', 'vip_get', + 'members_get', 'pool_get', + 'pool_health_monitors_get'), + api.quantum: ('subnet_get',)}) + def test_index_members(self): + self.set_up_expect() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL + '?tab=lbtabs__members') + + self.assertTemplateUsed(res, '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['memberstable_table'].data), + len(self.members.list())) + + @test.create_stubs({api.lbaas: ('pools_get', 'vip_get', + 'pool_health_monitors_get', + 'members_get', 'pool_get'), + api.quantum: ('subnet_get',)}) + def test_index_monitors(self): + self.set_up_expect() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL + '?tab=lbtabs__monitors') + + self.assertTemplateUsed(res, '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['monitorstable_table'].data), + len(self.monitors.list())) + + @test.create_stubs({api.lbaas: ('pools_get', 'members_get', + 'pool_health_monitors_get')}) + def test_index_exception_pools(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + + self.assertTemplateUsed(res, + '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['table'].data), 0) + + @test.create_stubs({api.lbaas: ('pools_get', 'members_get', + 'pool_health_monitors_get')}) + def test_index_exception_members(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL + '?tab=lbtabs__members') + + self.assertTemplateUsed(res, + '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['memberstable_table'].data), 0) + + @test.create_stubs({api.lbaas: ('pools_get', 'members_get', + 'pool_health_monitors_get')}) + def test_index_exception_monitors(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL + '?tab=lbtabs__monitors') + + self.assertTemplateUsed(res, + '%s/loadbalancers/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['monitorstable_table'].data), 0) + + @test.create_stubs({api.quantum: ('network_list_for_tenant',), + api.lbaas: ('pool_create', )}) + def test_add_pool_post(self): + pool = self.pools.first() + + subnet = self.subnets.first() + networks = [{'subnets': [subnet, ]}, ] + + api.quantum.network_list_for_tenant( + IsA(http.HttpRequest), subnet.tenant_id).AndReturn(networks) + + api.lbaas.pool_create( + IsA(http.HttpRequest), + name=pool.name, + description=pool.description, + subnet_id=pool.subnet_id, + protocol=pool.protocol, + lb_method=pool.lb_method, + admin_state_up=pool.admin_state_up).AndReturn(Pool(pool)) + + self.mox.ReplayAll() + + form_data = {'name': pool.name, + 'description': pool.description, + 'subnet_id': pool.subnet_id, + 'protocol': pool.protocol, + 'lb_method': pool.lb_method, + 'admin_state_up': pool.admin_state_up} + + res = self.client.post(reverse(self.ADDPOOL_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list_for_tenant',)}) + def test_add_pool_get(self): + subnet = self.subnets.first() + + networks = [{'subnets': [subnet, ]}, ] + + api.quantum.network_list_for_tenant( + IsA(http.HttpRequest), subnet.tenant_id).AndReturn(networks) + + self.mox.ReplayAll() + + res = self.client.get(reverse(self.ADDPOOL_PATH)) + + workflow = res.context['workflow'] + self.assertTemplateUsed(res, 'project/loadbalancers/addpool.html') + self.assertEqual(workflow.name, AddPool.name) + + expected_objs = ['', ] + self.assertQuerysetEqual(workflow.steps, expected_objs) + + @test.create_stubs({api.lbaas: ('pools_get', 'member_create'), + api.quantum: ('port_list',), + api.nova: ('server_list',)}) + def test_add_member_post(self): + member = self.members.first() + + server1 = self.AttributeDict({'id': + '12381d38-c3eb-4fee-9763-12de3338042e', + 'name': 'vm1'}) + server2 = self.AttributeDict({'id': + '12381d38-c3eb-4fee-9763-12de3338043e', + 'name': 'vm2'}) + + port1 = self.AttributeDict( + {'fixed_ips': [{'ip_address': member.address}]}) + + api.lbaas.pools_get(IsA(http.HttpRequest)).AndReturn(self.pools.list()) + + api.nova.server_list(IsA(http.HttpRequest)).AndReturn([server1, + server2]) + + api.quantum.port_list(IsA(http.HttpRequest), + device_id=server1.id).AndReturn([port1, ]) + + api.lbaas.member_create( + IsA(http.HttpRequest), + pool_id=member.pool_id, + address=member.address, + protocol_port=member.protocol_port, + weight=member.weight, + members=[server1.id], + admin_state_up=member.admin_state_up).AndReturn(Member(member)) + + self.mox.ReplayAll() + + form_data = {'pool_id': member.pool_id, + 'address': member.address, + 'protocol_port': member.protocol_port, + 'weight': member.weight, + 'members': [server1.id], + 'admin_state_up': member.admin_state_up} + + res = self.client.post(reverse(self.ADDMEMBER_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, self.INDEX_URL) + + @test.create_stubs({api.lbaas: ('pools_get',), + api.nova: ('server_list',)}) + def test_add_member_get(self): + server1 = self.AttributeDict({'id': + '12381d38-c3eb-4fee-9763-12de3338042e', + 'name': 'vm1'}) + server2 = self.AttributeDict({'id': + '12381d38-c3eb-4fee-9763-12de3338043e', + 'name': 'vm2'}) + + api.lbaas.pools_get(IsA(http.HttpRequest)).AndReturn(self.pools.list()) + api.nova.server_list( + IsA(http.HttpRequest)).AndReturn([server1, server2]) + + self.mox.ReplayAll() + + res = self.client.get(reverse(self.ADDMEMBER_PATH)) + + workflow = res.context['workflow'] + self.assertTemplateUsed(res, 'project/loadbalancers/addmember.html') + self.assertEqual(workflow.name, AddMember.name) + + expected_objs = ['', ] + self.assertQuerysetEqual(workflow.steps, expected_objs) diff --git a/openstack_dashboard/dashboards/project/loadbalancers/urls.py b/openstack_dashboard/dashboards/project/loadbalancers/urls.py new file mode 100644 index 000000000..461ea01be --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/urls.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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.urls.defaults import url, patterns + +from .views import IndexView +from .views import AddPoolView, AddMemberView, AddMonitorView, AddVipView +from .views import PoolDetailsView, VipDetailsView +from .views import MemberDetailsView, MonitorDetailsView + +urlpatterns = patterns( + 'openstack_dashboard.dashboards.project.loadbalancers.views', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^addpool$', AddPoolView.as_view(), name='addpool'), + url(r'^addvip/(?P[^/]+)/$', AddVipView.as_view(), name='addvip'), + url(r'^addmember$', AddMemberView.as_view(), name='addmember'), + url(r'^addmonitor$', AddMonitorView.as_view(), name='addmonitor'), + url(r'^pool/(?P[^/]+)/$', + PoolDetailsView.as_view(), name='pooldetails'), + url(r'^vip/(?P[^/]+)/$', + VipDetailsView.as_view(), name='vipdetails'), + url(r'^member/(?P[^/]+)/$', + MemberDetailsView.as_view(), name='memberdetails'), + url(r'^monitor/(?P[^/]+)/$', + MonitorDetailsView.as_view(), name='monitordetails')) diff --git a/openstack_dashboard/dashboards/project/loadbalancers/views.py b/openstack_dashboard/dashboards/project/loadbalancers/views.py new file mode 100644 index 000000000..9f87bcf44 --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/views.py @@ -0,0 +1,152 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 logging +import re + +from django import http +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import tables +from horizon import tabs +from horizon import workflows + +from openstack_dashboard import api + +from .workflows import AddPool, AddMember, AddMonitor, AddVip +from .tabs import LoadBalancerTabs, PoolDetailsTabs, VipDetailsTabs +from .tabs import MemberDetailsTabs, MonitorDetailsTabs +from .tables import DeleteMonitorLink + + +LOG = logging.getLogger(__name__) + + +class IndexView(tabs.TabView): + tab_group_class = (LoadBalancerTabs) + template_name = 'project/loadbalancers/details_tabs.html' + + def post(self, request, *args, **kwargs): + obj_ids = request.POST.getlist('object_ids') + action = request.POST['action'] + m = re.search('.delete([a-z]+)', action).group(1) + if obj_ids == []: + obj_ids.append(re.search('([0-9a-z-]+)$', action).group(1)) + if m == 'monitor': + for obj_id in obj_ids: + try: + api.lbaas.pool_health_monitor_delete(request, obj_id) + except: + exceptions.handle(request, + _('Unable to delete monitor.')) + if m == 'pool': + for obj_id in obj_ids: + try: + api.lbaas.pool_delete(request, obj_id) + except: + exceptions.handle(request, + _('Must delete Vip first.')) + if m == 'member': + for obj_id in obj_ids: + try: + api.lbaas.member_delete(request, obj_id) + except: + exceptions.handle(request, + _('Unable to delete member.')) + if m == 'vip': + for obj_id in obj_ids: + try: + vip_id = api.lbaas.pool_get(request, obj_id).vip_id + except: + exceptions.handle(request, + _('Unable to locate vip to delete.')) + if vip_id is not None: + try: + api.lbaas.vip_delete(request, vip_id) + except: + exceptions.handle(request, + _('Unable to delete vip.')) + return self.get(request, *args, **kwargs) + + +class AddPoolView(workflows.WorkflowView): + workflow_class = AddPool + template_name = "project/loadbalancers/addpool.html" + + def get_initial(self): + initial = super(AddPoolView, self).get_initial() + return initial + + +class AddVipView(workflows.WorkflowView): + workflow_class = AddVip + template_name = "project/loadbalancers/addvip.html" + + def get_context_data(self, **kwargs): + context = super(AddVipView, self).get_context_data(**kwargs) + return context + + def get_initial(self): + initial = super(AddVipView, self).get_initial() + initial['pool_id'] = self.kwargs['pool_id'] + try: + pool = api.lbaas.pool_get(self.request, initial['pool_id']) + initial['subnet'] = api.quantum.subnet_get( + self.request, pool.subnet_id).cidr + except: + initial['subnet'] = '' + msg = _('Unable to retrieve pool subnet.') + exceptions.handle(self.request, msg) + return initial + + +class AddMemberView(workflows.WorkflowView): + workflow_class = AddMember + template_name = "project/loadbalancers/addmember.html" + + def get_initial(self): + initial = super(AddMemberView, self).get_initial() + return initial + + +class AddMonitorView(workflows.WorkflowView): + workflow_class = AddMonitor + template_name = "project/loadbalancers/addmonitor.html" + + def get_initial(self): + initial = super(AddMonitorView, self).get_initial() + return initial + + +class PoolDetailsView(tabs.TabView): + tab_group_class = (PoolDetailsTabs) + template_name = 'project/loadbalancers/details_tabs.html' + + +class VipDetailsView(tabs.TabView): + tab_group_class = (VipDetailsTabs) + template_name = 'project/loadbalancers/details_tabs.html' + + +class MemberDetailsView(tabs.TabView): + tab_group_class = (MemberDetailsTabs) + template_name = 'project/loadbalancers/details_tabs.html' + + +class MonitorDetailsView(tabs.TabView): + tab_group_class = (MonitorDetailsTabs) + template_name = 'project/loadbalancers/details_tabs.html' diff --git a/openstack_dashboard/dashboards/project/loadbalancers/workflows.py b/openstack_dashboard/dashboards/project/loadbalancers/workflows.py new file mode 100644 index 000000000..f473935cb --- /dev/null +++ b/openstack_dashboard/dashboards/project/loadbalancers/workflows.py @@ -0,0 +1,448 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 logging +import re + +from django.utils.translation import ugettext as _ + +from horizon import exceptions +from horizon import forms +from horizon.utils import fields +from horizon import workflows + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class AddPoolAction(workflows.Action): + name = forms.CharField(max_length=80, label=_("Name")) + description = forms.CharField( + initial="", required=False, + max_length=80, label=_("Description")) + subnet_id = forms.ChoiceField(label=_("Subnet")) + protocol = forms.ChoiceField(label=_("Protocol")) + lb_method = forms.ChoiceField(label=_("Load Balancing Method")) + admin_state_up = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) + + def __init__(self, request, *args, **kwargs): + super(AddPoolAction, self).__init__(request, *args, **kwargs) + + tenant_id = request.user.tenant_id + + subnet_id_choices = [('', _("Select a Subnet"))] + try: + networks = api.quantum.network_list_for_tenant(request, tenant_id) + except: + exceptions.handle(request, + _('Unable to retrieve networks list.')) + for n in networks: + for s in n['subnets']: + subnet_id_choices.append((s.id, s.cidr)) + self.fields['subnet_id'].choices = subnet_id_choices + + protocol_choices = [('', _("Select a Protocol"))] + protocol_choices.append(('HTTP', 'HTTP')) + protocol_choices.append(('HTTPS', 'HTTPS')) + self.fields['protocol'].choices = protocol_choices + + lb_method_choices = [('', _("Select a Protocol"))] + lb_method_choices.append(('ROUND_ROBIN', 'ROUND_ROBIN')) + lb_method_choices.append(('LEAST_CONNECTIONS', 'LEAST_CONNECTIONS')) + lb_method_choices.append(('SOURCE_IP', 'SOURCE_IP')) + self.fields['lb_method'].choices = lb_method_choices + + class Meta: + name = _("PoolDetails") + permissions = ('openstack.services.network',) + help_text = _("Create Pool for current tenant.\n\n" + "Assign a name and description for the pool. " + "Choose one subnet where all members of this " + "pool must be on. " + "Select the protocol and load balancing method " + "for this pool. " + "Admin State is UP (checked) by defaul.t") + + +class AddPoolStep(workflows.Step): + action_class = AddPoolAction + contributes = ("name", "description", "subnet_id", + "protocol", "lb_method", "admin_state_up") + + def contribute(self, data, context): + context = super(AddPoolStep, self).contribute(data, context) + if data: + return context + + +class AddPool(workflows.Workflow): + slug = "addpool" + name = _("Add Pool") + finalize_button_name = _("Add") + success_message = _('Added Pool "%s".') + failure_message = _('Unable to add Pool "%s".') + success_url = "horizon:project:loadbalancers:index" + default_steps = (AddPoolStep,) + + def format_status_message(self, message): + name = self.context.get('name') + return message % name + + def handle(self, request, context): + try: + pool = api.lbaas.pool_create(request, **context) + context['name'] + return True + except: + exceptions.handle(request, + self.failure_message) + return False + + +class AddVipAction(workflows.Action): + name = forms.CharField(max_length=80, label=_("Name")) + description = forms.CharField( + initial="", required=False, + max_length=80, label=_("Description")) + floatip_address = forms.ChoiceField( + label=_("Vip Address from Floating IPs"), + widget=forms.Select(attrs={'disabled': 'disabled'}), + required=False) + other_address = fields.IPField(required=False, + initial="", + version=fields.IPv4, + mask=False) + protocol_port = forms.CharField(max_length=80, label=_("Protocol Port")) + protocol = forms.ChoiceField(label=_("Protocol")) + session_persistence = forms.ChoiceField( + required=False, initial={}, label=_("Session Persistence")) + cookie_name = forms.CharField( + initial="", required=False, + max_length=80, label=_("Cookie Name"), + help_text=_("Required for APP_COOKIE persistence;" + " Ignored otherwise.")) + connection_limit = forms.CharField( + max_length=80, label=_("Connection Limit")) + admin_state_up = forms.BooleanField( + label=_("Admin State"), initial=True, required=False) + + def __init__(self, request, *args, **kwargs): + super(AddVipAction, self).__init__(request, *args, **kwargs) + + self.fields['other_address'].label = _("Specify a free IP address" + " from %s" % + args[0]['subnet']) + + protocol_choices = [('', _("Select a Protocol"))] + protocol_choices.append(('HTTP', 'HTTP')) + protocol_choices.append(('HTTPS', 'HTTPS')) + self.fields['protocol'].choices = protocol_choices + + session_persistence_choices = [('', _("Set Session Persistence"))] + for mode in ('SOURCE_IP', 'HTTP_COOKIE', 'APP_COOKIE'): + session_persistence_choices.append((mode, mode)) + self.fields[ + 'session_persistence'].choices = session_persistence_choices + + floatip_address_choices = [('', _("Currently Not Supported"))] + self.fields['floatip_address'].choices = floatip_address_choices + + class Meta: + name = _("AddVip") + permissions = ('openstack.services.network',) + help_text = _("Create a vip (virtual IP) for this pool. " + "Assign a name and description for the vip. " + "Specify an IP address and port for the vip. " + "Choose the protocol and session persistence " + "method for the vip." + "Specify the max connections allowed. " + "Admin State is UP (checked) by default.") + + +class AddVipStep(workflows.Step): + action_class = AddVipAction + depends_on = ("pool_id", "subnet") + contributes = ("name", "description", "floatip_address", + "other_address", "protocol_port", "protocol", + "session_persistence", "cookie_name", + "connection_limit", "admin_state_up") + + def contribute(self, data, context): + context = super(AddVipStep, self).contribute(data, context) + return context + + +class AddVip(workflows.Workflow): + slug = "addvip" + name = _("Add Vip") + finalize_button_name = _("Add") + success_message = _('Added Vip "%s".') + failure_message = _('Unable to add Vip "%s".') + success_url = "horizon:project:loadbalancers:index" + default_steps = (AddVipStep,) + + def format_status_message(self, message): + name = self.context.get('name') + return message % name + + def handle(self, request, context): + if context['other_address'] == '': + context['address'] = context['floatip_address'] + else: + if not context['floatip_address'] == '': + self.failure_message = _('Only one address can be specified.' + 'Unable to add Vip %s.') + return False + else: + context['address'] = context['other_address'] + try: + pool = api.lbaas.pool_get(request, context['pool_id']) + context['subnet_id'] = pool['subnet_id'] + except: + context['subnet_id'] = None + exceptions.handle(request, + _('Unable to retrieve pool.')) + return False + + if context['session_persistence']: + stype = context['session_persistence'] + if stype == 'APP_COOKIE': + if context['cookie_name'] == "": + self.failure_message = _('Cookie name must be specified ' + 'with APP_COOKIE persistence.') + return False + else: + cookie = context['cookie_name'] + context['session_persistence'] = {'type': stype, + 'cookie_name': cookie} + else: + context['session_persistence'] = {'type': stype} + else: + context['session_persistence'] = {} + + try: + api.lbaas.vip_create(request, **context) + return True + except: + exceptions.handle(request, + self.failure_message) + return False + + +class AddMemberAction(workflows.Action): + pool_id = forms.ChoiceField(label=_("Pool")) + members = forms.MultipleChoiceField( + label=_("Member(s)"), + required=True, + initial=["default"], + widget=forms.CheckboxSelectMultiple(), + help_text=_("Select members for this pool ")) + weight = forms.CharField(max_length=80, label=_("Weight")) + protocol_port = forms.CharField(max_length=80, label=_("Protocol Port")) + admin_state_up = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) + + def __init__(self, request, *args, **kwargs): + super(AddMemberAction, self).__init__(request, *args, **kwargs) + + pool_id_choices = [('', _("Select a Pool"))] + try: + pools = api.lbaas.pools_get(request) + except: + pools = [] + exceptions.handle(request, + _('Unable to retrieve pools list.')) + pools = sorted(pools, + key=lambda pool: pool.name) + for p in pools: + pool_id_choices.append((p.id, p.name)) + self.fields['pool_id'].choices = pool_id_choices + + members_choices = [] + try: + servers = api.nova.server_list(request) + except: + servers = [] + exceptions.handle(request, + _('Unable to retrieve instances list.')) + + if len(servers) == 0: + self.fields['members'].label = _("No servers available. " + "Click Add to cancel.") + self.fields['members'].required = False + self.fields['members'].help_text = _("Select members " + "for this pool ") + self.fields['pool_id'].required = False + self.fields['weight'].required = False + self.fields['protocol_port'].required = False + return + + for m in servers: + members_choices.append((m.id, m.name)) + self.fields['members'].choices = sorted( + members_choices, + key=lambda member: member[1]) + + class Meta: + name = _("MemberDetails") + permissions = ('openstack.services.network',) + help_text = _("Add member to selected pool.\n\n" + "Choose one or more listed instances to be " + "added to the pool as member(s). " + "Assign a numeric weight for this member " + "Specify the port number the member(s) " + "operate on; e.g., 80.") + + +class AddMemberStep(workflows.Step): + action_class = AddMemberAction + contributes = ("pool_id", "members", "protocol_port", "weight", + "admin_state_up") + + def contribute(self, data, context): + context = super(AddMemberStep, self).contribute(data, context) + return context + + +class AddMember(workflows.Workflow): + slug = "addmember" + name = _("Add Member") + finalize_button_name = _("Add") + success_message = _('Added Member "%s".') + failure_message = _('Unable to add Member %s.') + success_url = "horizon:project:loadbalancers:index" + default_steps = (AddMemberStep,) + + def format_status_message(self, message): + member_id = self.context.get('member_id') + return message % member_id + + def handle(self, request, context): + if context['members'] == []: + self.failure_message = _('No instances available.%s') + context['member_id'] = '' + return False + + for m in context['members']: + params = {'device_id': m} + try: + plist = api.quantum.port_list(request, **params) + except: + plist = [] + exceptions.handle(request, + _('Unable to retrieve ports list.')) + return False + if plist: + context['address'] = plist[0].fixed_ips[0]['ip_address'] + try: + context['member_id'] = api.lbaas.member_create( + request, **context).id + except: + exceptions.handle(request, + self.failure_message) + return False + return True + + +class AddMonitorAction(workflows.Action): + pool_id = forms.ChoiceField(label=_("Pool")) + type = forms.ChoiceField(label=_("Type")) + delay = forms.CharField(max_length=80, label=_("Delay")) + timeout = forms.CharField(max_length=80, label=_("Timeout")) + max_retries = forms.CharField(max_length=80, + label=_("Max Retries (1~10)")) + http_method = forms.ChoiceField( + initial="GET", required=False, label=_("HTTP Method")) + url_path = forms.CharField( + initial="/", required=False, max_length=80, label=_("URL")) + expected_codes = forms.CharField( + initial="200", required=False, max_length=80, + label=_("Expected HTTP Status Codes")) + admin_state_up = forms.BooleanField(label=_("Admin State"), + initial=True, required=False) + + def __init__(self, request, *args, **kwargs): + super(AddMonitorAction, self).__init__(request, *args, **kwargs) + + pool_id_choices = [('', _("Select a Pool"))] + try: + pools = api.lbaas.pools_get(request) + except: + exceptions.handle(request, + _('Unable to retrieve pools list.')) + for p in pools: + pool_id_choices.append((p.id, p.name)) + self.fields['pool_id'].choices = pool_id_choices + + type_choices = [('', _("Select Type"))] + type_choices.append(('PING', 'PING')) + type_choices.append(('TCP', 'TCP')) + type_choices.append(('HTTP', 'HTTP')) + type_choices.append(('HTTPS', 'HTTPS')) + self.fields['type'].choices = type_choices + + http_method_choices = [('', _("Select HTTP Method"))] + http_method_choices.append(('GET', 'GET')) + self.fields['http_method'].choices = http_method_choices + + class Meta: + name = _("MonitorDetails") + permissions = ('openstack.services.network',) + help_text = _("Create a monitor for a pool.\n\n" + "Select target pool and type of monitoring. " + "Specify delay, timeout, and retry limits " + "required by the monitor. " + "Specify method, URL path, and expected " + "HTTP codes upon success.") + + +class AddMonitorStep(workflows.Step): + action_class = AddMonitorAction + contributes = ("pool_id", "type", "delay", "timeout", "max_retries", + "http_method", "url_path", "expected_codes", + "admin_state_up") + + def contribute(self, data, context): + context = super(AddMonitorStep, self).contribute(data, context) + if data: + return context + + +class AddMonitor(workflows.Workflow): + slug = "addmonitor" + name = _("Add Monitor") + finalize_button_name = _("Add") + success_message = _('Added Monitor "%s".') + failure_message = _('Unable to add Monitor "%s".') + success_url = "horizon:project:loadbalancers:index" + default_steps = (AddMonitorStep,) + + def format_status_message(self, message): + monitor_id = self.context.get('monitor_id') + return message % monitor_id + + def handle(self, request, context): + try: + context['monitor_id'] = api.lbaas.pool_health_monitor_create( + request, **context).get('id') + return True + except: + exceptions.handle(request, + self.failure_message) + return False diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index b85fa80cc..74b5e8557 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -104,6 +104,13 @@ OPENSTACK_HYPERVISOR_FEATURES = { 'can_encrypt_volumes': False } +# The OPENSTACK_QUANTUM_NETWORK settings can be used to enable optional +# services provided by quantum. Currently only the load balancer service +# is available. +OPENSTACK_QUANTUM_NETWORK = { + 'enable_lb': True +} + # OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints # in the Keystone service catalog. Use this setting when Horizon is running # external to the OpenStack environment. The default is 'internalURL'. diff --git a/openstack_dashboard/test/api_tests/lbaas_tests.py b/openstack_dashboard/test/api_tests/lbaas_tests.py new file mode 100644 index 000000000..b13017bbd --- /dev/null +++ b/openstack_dashboard/test/api_tests/lbaas_tests.py @@ -0,0 +1,363 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013, Big Switch Networks, Inc. +# +# 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 mox import IsA +from django import http + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test +from openstack_dashboard.api.lbaas import Vip, Pool, Member, PoolMonitor + +from quantumclient.v2_0.client import Client as quantumclient + + +class LbaasApiTests(test.APITestCase): + @test.create_stubs({quantumclient: ('create_vip',)}) + def test_vip_create(self): + vip1 = self.api_vips.first() + form_data = {'address': vip1['address'], + 'name': vip1['name'], + 'description': vip1['description'], + 'subnet_id': vip1['subnet_id'], + 'protocol_port': vip1['protocol_port'], + 'protocol': vip1['protocol'], + 'pool_id': vip1['pool_id'], + 'session_persistence': vip1['session_persistence'], + 'connection_limit': vip1['connection_limit'], + 'admin_state_up': vip1['admin_state_up'] + } + vip = {'vip': self.api_vips.first()} + quantumclient.create_vip({'vip': form_data}).AndReturn(vip) + self.mox.ReplayAll() + + ret_val = api.lbaas.vip_create(self.request, **form_data) + self.assertIsInstance(ret_val, api.lbaas.Vip) + + @test.create_stubs({quantumclient: ('list_vips',)}) + def test_vips_get(self): + vips = {'vips': [{'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.0.100', + 'name': 'vip1name', + 'description': 'vip1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'pool_id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'connection_limit': '10', + 'admin_state_up': True + }, ]} + quantumclient.list_vips().AndReturn(vips) + self.mox.ReplayAll() + + ret_val = api.lbaas.vips_get(self.request) + for v in ret_val: + self.assertIsInstance(v, api.lbaas.Vip) + + @test.create_stubs({quantumclient: ('show_vip',)}) + def test_vip_get(self): + vip = {'vip': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.0.100', + 'name': 'vip1name', + 'description': 'vip1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'pool_id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'connection_limit': '10', + 'admin_state_up': True + }} + quantumclient.show_vip(vip['vip']['id']).AndReturn(vip) + self.mox.ReplayAll() + + ret_val = api.lbaas.vip_get(self.request, vip['vip']['id']) + self.assertIsInstance(ret_val, api.lbaas.Vip) + + @test.create_stubs({quantumclient: ('update_vip',)}) + def test_vip_update(self): + form_data = {'address': '10.0.0.100', + 'name': 'vip1name', + 'description': 'vip1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'pool_id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'connection_limit': '10', + 'admin_state_up': True + } + + vip = {'vip': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.0.100', + 'name': 'vip1name', + 'description': 'vip1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol_port': '80', + 'protocol': 'HTTP', + 'pool_id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'connection_limit': '10', + 'admin_state_up': True + }} + quantumclient.update_vip(vip['vip']['id'], form_data).AndReturn(vip) + self.mox.ReplayAll() + + ret_val = api.lbaas.vip_update(self.request, + vip['vip']['id'], **form_data) + self.assertIsInstance(ret_val, api.lbaas.Vip) + + @test.create_stubs({quantumclient: ('create_pool',)}) + def test_pool_create(self): + form_data = {'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTP', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True + } + + pool = {'pool': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTP', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True + }} + quantumclient.create_pool({'pool': form_data}).AndReturn(pool) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_create(self.request, **form_data) + self.assertIsInstance(ret_val, api.lbaas.Pool) + + @test.create_stubs({quantumclient: ('list_pools',)}) + def test_pools_get(self): + pools = {'pools': [{ + 'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTP', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True}, ]} + quantumclient.list_pools().AndReturn(pools) + self.mox.ReplayAll() + + ret_val = api.lbaas.pools_get(self.request) + for v in ret_val: + self.assertIsInstance(v, api.lbaas.Pool) + + @test.create_stubs({quantumclient: ('show_pool',)}) + def test_pool_get(self): + pool = {'pool': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTP', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True + }} + quantumclient.show_pool(pool['pool']['id']).AndReturn(pool) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_get(self.request, pool['pool']['id']) + self.assertIsInstance(ret_val, api.lbaas.Pool) + + @test.create_stubs({quantumclient: ('update_pool',)}) + def test_pool_update(self): + form_data = {'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTPS', + 'lb_method': 'LEAST_CONNECTION', + 'admin_state_up': True + } + + pool = {'pool': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'pool1name', + 'description': 'pool1description', + 'subnet_id': '12381d38-c3eb-4fee-9763-12de3338041e', + 'protocol': 'HTTPS', + 'lb_method': 'LEAST_CONNECTION', + 'admin_state_up': True + }} + quantumclient.update_pool(pool['pool']['id'], + form_data).AndReturn(pool) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_update(self.request, + pool['pool']['id'], **form_data) + self.assertIsInstance(ret_val, api.lbaas.Pool) + + @test.create_stubs({quantumclient: ('create_health_monitor', + 'associate_health_monitor')}) + def test_pool_health_monitor_create(self): + form_data = {'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/monitor', + 'expected_codes': '200', + 'admin_state_up': True + } + form_data_with_pool_id = { + 'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/monitor', + 'expected_codes': '200', + 'admin_state_up': True} + monitor = {'health_monitor': { + 'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/monitor', + 'expected_codes': '200', + 'admin_state_up': True}} + monitor_id = {'health_monitor': { + 'id': 'abcdef-c3eb-4fee-9763-12de3338041e'}} + quantumclient.create_health_monitor({ + 'health_monitor': form_data}).AndReturn(monitor) + quantumclient.associate_health_monitor( + form_data_with_pool_id['pool_id'], monitor_id) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_health_monitor_create( + self.request, **form_data_with_pool_id) + self.assertIsInstance(ret_val, api.lbaas.PoolMonitor) + + @test.create_stubs({quantumclient: ('list_health_monitors',)}) + def test_pool_health_monitors_get(self): + monitors = {'health_monitors': [ + {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/monitor', + 'expected_codes': '200', + 'admin_state_up': True}, ]} + + quantumclient.list_health_monitors().AndReturn(monitors) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_health_monitors_get(self.request) + for v in ret_val: + self.assertIsInstance(v, api.lbaas.PoolMonitor) + + @test.create_stubs({quantumclient: ('show_health_monitor',)}) + def test_pool_health_monitor_get(self): + monitor = {'health_monitor': + {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/monitor', + 'expected_codes': '200', + 'admin_state_up': True}} + quantumclient.show_health_monitor( + monitor['health_monitor']['id']).AndReturn(monitor) + self.mox.ReplayAll() + + ret_val = api.lbaas.pool_health_monitor_get( + self.request, monitor['health_monitor']['id']) + self.assertIsInstance(ret_val, api.lbaas.PoolMonitor) + + @test.create_stubs({quantumclient: ('create_member', )}) + def test_member_create(self): + form_data = {'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.2', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True + } + + member = {'member': + {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.2', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True}} + + quantumclient.create_member({'member': form_data}).AndReturn(member) + self.mox.ReplayAll() + + ret_val = api.lbaas.member_create(self.request, **form_data) + self.assertIsInstance(ret_val, api.lbaas.Member) + + @test.create_stubs({quantumclient: ('list_members',)}) + def test_members_get(self): + members = {'members': [ + {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.2', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True + }, ]} + quantumclient.list_members().AndReturn(members) + self.mox.ReplayAll() + + ret_val = api.lbaas.members_get(self.request) + for v in ret_val: + self.assertIsInstance(v, api.lbaas.Member) + + @test.create_stubs({quantumclient: ('show_member',)}) + def test_member_get(self): + member = {'member': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.2', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True}} + quantumclient.show_member(member['member']['id']).AndReturn(member) + self.mox.ReplayAll() + + ret_val = api.lbaas.member_get(self.request, member['member']['id']) + self.assertIsInstance(ret_val, api.lbaas.Member) + + @test.create_stubs({quantumclient: ('update_member',)}) + def test_member_update(self): + form_data = {'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.4', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True + } + + member = {'member': {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'pool_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'address': '10.0.1.2', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True + }} + + quantumclient.update_member(member['member']['id'], + form_data).AndReturn(member) + self.mox.ReplayAll() + + ret_val = api.lbaas.member_update(self.request, + member['member']['id'], **form_data) + self.assertIsInstance(ret_val, api.lbaas.Member) diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index cdbd49312..eee2b17fa 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -74,6 +74,10 @@ OPENSTACK_KEYSTONE_BACKEND = { 'can_edit_project': True } +OPENSTACK_QUANTUM_NETWORK = { + 'enable_lb': True +} + OPENSTACK_HYPERVISOR_FEATURES = { 'can_set_mount_point': True, diff --git a/openstack_dashboard/test/test_data/quantum_data.py b/openstack_dashboard/test/test_data/quantum_data.py index 5b5acdd2f..897de70ca 100644 --- a/openstack_dashboard/test/test_data/quantum_data.py +++ b/openstack_dashboard/test/test_data/quantum_data.py @@ -16,7 +16,8 @@ import copy from openstack_dashboard.api.quantum import (Network, Subnet, Port, Router, FloatingIp) - +from openstack_dashboard.api.lbaas import (Pool, Vip, Member, + PoolMonitor) from .utils import TestDataContainer @@ -27,6 +28,10 @@ def data(TEST): TEST.ports = TestDataContainer() TEST.routers = TestDataContainer() TEST.q_floating_ips = TestDataContainer() + TEST.pools = TestDataContainer() + TEST.vips = TestDataContainer() + TEST.members = TestDataContainer() + TEST.monitors = TestDataContainer() # data return by quantumclient TEST.api_networks = TestDataContainer() @@ -34,6 +39,10 @@ def data(TEST): TEST.api_ports = TestDataContainer() TEST.api_routers = TestDataContainer() TEST.api_q_floating_ips = TestDataContainer() + TEST.api_pools = TestDataContainer() + TEST.api_vips = TestDataContainer() + TEST.api_members = TestDataContainer() + TEST.api_monitors = TestDataContainer() #------------------------------------------------------------ # 1st network @@ -235,3 +244,112 @@ def data(TEST): 'router_id': router_dict['id']} TEST.api_q_floating_ips.add(fip_dict) TEST.q_floating_ips.add(FloatingIp(fip_dict)) + + #------------------------------------------------------------ + # LBaaS + + # 1st pool + pool_dict = {'id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'tenant_id': '1', + 'vip_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'pool1', + 'description': 'pool description', + 'subnet_id': TEST.subnets.first().id, + 'protocol': 'HTTP', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True} + TEST.api_pools.add(pool_dict) + TEST.pools.add(Pool(pool_dict)) + + # 1st vip + vip_dict = {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'vip1', + 'address': '10.0.0.100', + 'description': 'vip description', + 'subnet_id': TEST.subnets.first().id, + 'protocol_port': '80', + 'protocol': pool_dict['protocol'], + 'pool_id': pool_dict['id'], + 'session_persistence': {'type': 'SOURCE_IP', + 'cookie_name': 'jssessionid'}, + 'connection_limit': '10', + 'admin_state_up': True} + TEST.api_vips.add(vip_dict) + TEST.vips.add(Vip(vip_dict)) + + # 1st member + member_dict = {'id': '78a46e5e-eb1a-418a-88c7-0e3f5968b08', + 'tenant_id': '1', + 'pool_id': pool_dict['id'], + 'address': '10.0.0.11', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True} + TEST.api_members.add(member_dict) + TEST.members.add(Member(member_dict)) + + # 2nd member + member_dict = {'id': '41ac1f8d-6d9c-49a4-a1bf-41955e651f91', + 'tenant_id': '1', + 'pool_id': pool_dict['id'], + 'address': '10.0.0.12', + 'protocol_port': '80', + 'weight': '10', + 'admin_state_up': True} + TEST.api_members.add(member_dict) + TEST.members.add(Member(member_dict)) + + # 2nd pool + pool_dict = {'id': '8913dde8-4915-4b90-8d3e-b95eeedb0d50', + 'tenant_id': '1', + 'vip_id': 'f0881d38-c3eb-4fee-9763-12de3338041d', + 'name': 'pool2', + 'description': 'pool description', + 'subnet_id': TEST.subnets.first().id, + 'protocol': 'HTTPS', + 'lb_method': 'ROUND_ROBIN', + 'admin_state_up': True} + TEST.api_pools.add(pool_dict) + TEST.pools.add(Pool(pool_dict)) + + # 1st vip + vip_dict = {'id': 'f0881d38-c3eb-4fee-9763-12de3338041d', + 'name': 'vip2', + 'address': '10.0.0.110', + 'description': 'vip description', + 'subnet_id': TEST.subnets.first().id, + 'protocol_port': '80', + 'protocol': pool_dict['protocol'], + 'pool_id': pool_dict['id'], + 'session_persistence': {'type': 'APP_COOKIE', + 'cookie_name': 'jssessionid'}, + 'connection_limit': '10', + 'admin_state_up': True} + TEST.api_vips.add(vip_dict) + TEST.vips.add(Vip(vip_dict)) + + # 1st monitor + monitor_dict = {'id': 'd4a0500f-db2b-4cc4-afcf-ec026febff96', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/', + 'expected_codes': '200', + 'admin_state_up': True} + TEST.api_monitors.add(monitor_dict) + TEST.monitors.add(PoolMonitor(monitor_dict)) + + # 2nd monitor + monitor_dict = {'id': 'd4a0500f-db2b-4cc4-afcf-ec026febff97', + 'type': 'PING', + 'delay': '10', + 'timeout': '10', + 'max_retries': '10', + 'http_method': 'GET', + 'url_path': '/', + 'expected_codes': '200', + 'admin_state_up': True} + TEST.api_monitors.add(monitor_dict) + TEST.monitors.add(PoolMonitor(monitor_dict))