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