From 1c270af393d332288b0ea570ece0a5a75608a578 Mon Sep 17 00:00:00 2001 From: chiragtayal Date: Tue, 3 Jan 2017 17:09:09 -0800 Subject: [PATCH] Programmable Fabric Panel in Cisco dashboard It displays Nexus Programmable Fabric information for troubleshooting and provide fabric summary, topology, network profile information Change-Id: I84e1d2b1b163104be5b84411056141aa3ada6f95 --- AUTHORS | 1 + horizon_cisco_ui/cisco/dfa/dfa_client.py | 173 ++++++++++--- horizon_cisco_ui/cisco/dfa/forms.py | 63 +++++ horizon_cisco_ui/cisco/dfa/panel.py | 43 ++++ horizon_cisco_ui/cisco/dfa/tables.py | 234 ++++++++++++++++++ horizon_cisco_ui/cisco/dfa/tabs.py | 133 ++++++++++ .../cisco/dfa/templates/dfa/_associate.html | 7 + .../dfa/templates/dfa/_detail_overview.html | 19 ++ .../dfa/_detail_profile_overview.html | 18 ++ .../dfa/templates/dfa/_disassociate.html | 6 + .../cisco/dfa/templates/dfa/_instance.html | 19 ++ .../dfa/templates/dfa/_profile_overview.html | 4 + .../cisco/dfa/templates/dfa/_reason.html | 14 ++ .../cisco/dfa/templates/dfa/associate.html | 7 + .../cisco/dfa/templates/dfa/detail.html | 16 ++ .../dfa/templates/dfa/detailprofile.html | 12 + .../cisco/dfa/templates/dfa/disassociate.html | 7 + .../dfa/templates/dfa/enablerinfo_tables.html | 14 ++ .../cisco/dfa/templates/dfa/index.html | 12 + .../cisco/dfa/test/dfa_client_test.py | 108 +++++++- .../cisco/dfa/test/dfa_horizon_test.py | 132 ++++++++++ horizon_cisco_ui/cisco/dfa/test/test_data.py | 98 ++++++++ horizon_cisco_ui/cisco/dfa/urls.py | 33 +++ horizon_cisco_ui/cisco/dfa/views.py | 191 ++++++++++++++ horizon_cisco_ui/cisco/dfa/workflows.py | 8 +- horizon_cisco_ui/enabled/_6020_dfa.py | 5 + 26 files changed, 1333 insertions(+), 44 deletions(-) create mode 100644 horizon_cisco_ui/cisco/dfa/forms.py create mode 100644 horizon_cisco_ui/cisco/dfa/panel.py create mode 100644 horizon_cisco_ui/cisco/dfa/tables.py create mode 100644 horizon_cisco_ui/cisco/dfa/tabs.py create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_associate.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_overview.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_profile_overview.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_disassociate.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_instance.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_profile_overview.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/_reason.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/associate.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/detail.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/detailprofile.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/disassociate.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/enablerinfo_tables.html create mode 100644 horizon_cisco_ui/cisco/dfa/templates/dfa/index.html create mode 100644 horizon_cisco_ui/cisco/dfa/test/dfa_horizon_test.py create mode 100644 horizon_cisco_ui/cisco/dfa/test/test_data.py create mode 100644 horizon_cisco_ui/cisco/dfa/urls.py create mode 100644 horizon_cisco_ui/cisco/dfa/views.py create mode 100644 horizon_cisco_ui/enabled/_6020_dfa.py diff --git a/AUTHORS b/AUTHORS index 4c41080..067e5b3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Rob Cresswell Saksham Varma +Chirag Tayal diff --git a/horizon_cisco_ui/cisco/dfa/dfa_client.py b/horizon_cisco_ui/cisco/dfa/dfa_client.py index 8766ae6..dd12ff2 100644 --- a/horizon_cisco_ui/cisco/dfa/dfa_client.py +++ b/horizon_cisco_ui/cisco/dfa/dfa_client.py @@ -14,12 +14,10 @@ # under the License. import ConfigParser -import json import logging import platform -from horizon import exceptions -from horizon.utils.memoized import memoized +from django.utils.translation import ugettext_lazy as _ try: import oslo_messaging as messaging @@ -30,11 +28,14 @@ try: except ImportError: from oslo.config import cfg +from horizon import exceptions +from horizon.utils.memoized import memoized + LOG = logging.getLogger(__name__) class DFAClient(object): - """Represents fabric enabler command line interface.""" + """Represents Nexus Fabric Enabler RPC Client.""" def __init__(self): self.setup_client() @@ -58,30 +59,144 @@ class DFAClient(object): return self.clnt - def associate_profile_with_network(self, network): - context = {} - args = json.dumps(network) - try: - resp = self.clnt.call(context, - 'associate_profile_with_network', - msg=args) - return resp - except (messaging.MessagingException, messaging.RemoteError, - messaging.MessagingTimeout): - LOG.error("RPC: Request to associate profile with network" - " failed.") - raise exceptions.NotAvailable("RPC to Fabric Enabler failed") - def get_config_profiles_detail(self): - '''Get all config Profiles details from the Fabric Enabler''' +@memoized +def dfaclient(): + return DFAClient().clnt - context = {} - args = json.dumps({}) - try: - resp = self.clnt.call(context, 'get_config_profiles_detail', - msg=args) - return resp - except (messaging.MessagingException, messaging.RemoteError, - messaging.MessagingTimeout): - LOG.error("RPC: Request for detailed Config Profiles failed.") - raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + +def associate_profile_with_network(network): + '''Associate Network Profile with network''' + + try: + resp = dfaclient().call({}, 'associate_profile_with_network', + msg=network) + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request to associate_profile_with_network failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def do_associate_dci_id_to_project(tenant): + '''Associate DCI ID to Project''' + + try: + resp = dfaclient().cast({}, 'associate_dci_id_to_project', + msg=tenant) + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request to associate DCI_ID to Project failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_fabric_summary(): + '''Get fabric details from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_fabric_summary', msg={}) + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for Fabric Summary failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_per_config_profile_detail(cfg_profile): + '''Get all config Profiles details from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_per_config_profile_detail', + msg=cfg_profile) + return resp + except Exception as e: + mess = (_('%(reason)s') % {"reason": e}) + LOG.error(mess) + reason = mess.partition("Traceback")[0] + raise exceptions.NotAvailable(reason) + + +def get_config_profiles_detail(): + '''Get all config Profiles details from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_config_profiles_detail', msg={}) + return resp + except Exception as e: + mess = (_('%(reason)s') % {"reason": e}) + reason = mess.partition("Traceback")[0] + raise exceptions.NotAvailable(reason) + + +def get_project_details(tenant): + '''Get project details for a tenant from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_project_detail', msg=tenant) + if not resp: + raise exceptions.NotFound("Project Not Found in Fabric \ + Enabler") + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for project details failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_network_by_tenant_id(tenant): + '''Get all networks for a tenant from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_all_networks_for_tenant', msg=tenant) + if resp is False: + raise exceptions.NotFound("Project Not Found in Fabric \ + Enabler") + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for project details failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_instance_by_tenant_id(tenant): + '''Get all instances for a tenant from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_instance_by_tenant_id', msg=tenant) + if resp is False: + raise exceptions.NotFound("Project Not Found in Fabric \ + Enabler") + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for project details failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_agents_details(): + '''Get all Enabler agents from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_agents_details', msg={}) + if not resp: + raise exceptions.NotFound("No Agents found for Fabric Enabler") + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for project details failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") + + +def get_agent_details_per_host(agent): + '''Get Enabler agent for a host from the Fabric Enabler''' + + try: + resp = dfaclient().call({}, 'get_agent_details_per_host', msg=agent) + if not resp: + raise exceptions.NotFound("No Agent found for Fabric Enabler") + return resp + except (messaging.MessagingException, messaging.RemoteError, + messaging.MessagingTimeout): + LOG.error("RPC: Request for project details failed.") + raise exceptions.NotAvailable("RPC to Fabric Enabler failed") diff --git a/horizon_cisco_ui/cisco/dfa/forms.py b/horizon_cisco_ui/cisco/dfa/forms.py new file mode 100644 index 0000000..145b69c --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/forms.py @@ -0,0 +1,63 @@ +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon_cisco_ui.cisco.dfa import dfa_client + +LOG = logging.getLogger(__name__) + + +class AssociateDCI(forms.SelfHandlingForm): + project_id = forms.CharField(label=_("Project ID "), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + dci_id = forms.IntegerField(label=_("DCI ID"), min_value=1, + max_value=1600000) + + def handle(self, request, data): + try: + LOG.debug('request = %(req)s, params = %(params)s', + {'req': request, 'params': data}) + tenant = dict(tenant_id=request.user.project_id, + tenant_name=request.user.project_name, + dci_id=data['dci_id']) + dfa_client.do_associate_dci_id_to_project(tenant) + except Exception: + redirect = reverse('horizon:cisco:dfa:index') + msg = _('Failed to associate DCI ID to project') + exceptions.handle(request, msg, redirect=redirect) + + return True + + +class DisassociateDCI(forms.SelfHandlingForm): + + def handle(self, request, data): + try: + LOG.debug('request = %(req)s, params = %(params)s', + {'req': request, 'params': data}) + tenant = dict(tenant_id=request.user.project_id, + tenant_name=request.user.project_name, + dci_id=0) + dfa_client.do_associate_dci_id_to_project(tenant) + except Exception: + redirect = reverse('horizon:cisco:dfa:index') + msg = _('Failed to disassociate DCI ID from project') + exceptions.handle(request, msg, redirect=redirect) + + return True diff --git a/horizon_cisco_ui/cisco/dfa/panel.py b/horizon_cisco_ui/cisco/dfa/panel.py new file mode 100644 index 0000000..ab5eca7 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/panel.py @@ -0,0 +1,43 @@ +# 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.utils.translation import ugettext_lazy as _ + +import horizon +from horizon_cisco_ui.cisco import dashboard +import logging +import os.path + +LOG = logging.getLogger(__name__) + + +class DFA(horizon.Panel): + name = _("Programmable Fabric") + slug = "dfa" + permissions = ('openstack.services.network',) + + def allowed(self, context): + request = context['request'] + if not request.user.has_perms(self.permissions): + return False + try: + if not os.path.isfile('/etc/saf/enabler_conf.ini'): + return False + except Exception: + LOG.error("Exception occured trying to find the Nexus Fabric " + "Enabler Configuration File") + return False + if not super(DFA, self).allowed(context): + return False + return True + +dashboard.Cisco.register(DFA) diff --git a/horizon_cisco_ui/cisco/dfa/tables.py b/horizon_cisco_ui/cisco/dfa/tables.py new file mode 100644 index 0000000..8666154 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/tables.py @@ -0,0 +1,234 @@ +# 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 import template +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +import logging + +LOG = logging.getLogger(__name__) + + +class AssociateDCIAction(tables.LinkAction): + name = "associate" + verbose_name = _("Associate DCI ID") + url = "horizon:cisco:dfa:associate" + classes = ("ajax-modal",) + icon = "link" + + +class DisassociateDCIAction(tables.LinkAction): + name = "disassociate" + verbose_name = _("Disassociate DCI ID") + url = "horizon:cisco:dfa:disassociate" + classes = ("ajax-modal",) + icon = "link" + + +class SearchFilterAction(tables.FilterAction): + name = "searchfilter" + + +def get_vdp(obj): + if obj.get('vdp_vlan') > 0: + return obj.get('vdp_vlan') + + if 'reason' in obj: + template_name = 'cisco/dfa/_vdp_reason.html' + context = { + "vdp": obj.get('vdp_vlan'), + "id": obj.get('port_id'), + "reason": obj.get('reason') + } + return template.loader.render_to_string(template_name, context) + return obj.get('vdp_vlan') + + +def get_result(obj): + if obj.get('result') == 'SUCCESS': + return obj.get('result') + + if 'network_id' in obj: + obj_id = obj.get('network_id') + elif 'project_id' in obj: + obj_id = obj.get('project_id') + elif 'port_id' in obj: + obj_id = obj.get('port_id') + else: + obj_id = 'none' + + if 'reason' in obj: + template_name = 'cisco/dfa/_reason.html' + context = { + "result": obj.get('result'), + "id": obj_id, + "reason": obj.get('reason') + } + return template.loader.render_to_string(template_name, context) + return obj.get('result') + + +def get_instance_info(obj): + if 'instance_id' and 'veth_intf' in obj: + template_name = 'cisco/dfa/_instance.html' + context = { + "name": obj.get('name'), + "nw_name": obj.get('network_name'), + "id": ''.join(e for e in obj.get('instance_id') if e.isalnum()), + "mac": obj.get('mac'), + "ip": obj.get('ip'), + "port": obj.get('port_id'), + "host_port": ', '.join((obj.get('host'), obj.get('veth_intf'))), + "TOR_port": ', '.join((obj.get('remote_system_name'), + obj.get('remote_port'))) + } + return template.loader.render_to_string(template_name, context) + + return obj.get('name') + + +class FabricSummaryTable(tables.DataTable): + key = tables.Column("key", sortable=False, + verbose_name=_("Fabric Property")) + value = tables.Column("value", + verbose_name=_("Value")) + + def get_object_id(self, obj): + return obj.get('key') + + class Meta(object): + name = "fabricsummary" + verbose_name = _("Fabric Summary") + table_actions = (SearchFilterAction, ) + multi_select = False + + +class CFGProfileTable(tables.DataTable): + profile_name = tables.Column("profileName", sortable=False, + verbose_name=_("Profile Name"), + link='horizon:cisco:dfa:detailprofile') + + def get_object_id(self, obj): + return ':'.join((obj.get('profileName'), obj.get('profileType'))) + + class Meta(object): + name = "cfgprofile" + verbose_name = _("CFGProfile") + table_actions = (SearchFilterAction, ) + multi_select = False + + +class ProjectTable(tables.DataTable): + project_name = tables.Column("project_name", + verbose_name=_("Project Name")) + project_id = tables.Column("project_id", verbose_name=_("Project ID")) + seg_id = tables.Column("seg_id", verbose_name=_("L3 VNI")) + dci_id = tables.Column("dci_id", verbose_name=_("DCI ID")) + result = tables.Column(get_result, verbose_name=_("Result")) + + def get_object_id(self, obj): + return obj.get('project_id') + + class Meta(object): + name = "projecttable" + hidden_title = False + verbose_name = _("Projects") + row_actions = (AssociateDCIAction, DisassociateDCIAction, ) + + +class NetworkTable(tables.DataTable): + network_name = tables.Column("network_name", verbose_name=_("Name")) + network_id = tables.Column("network_id", verbose_name=_("ID")) + config_profile = tables.Column("config_profile", + verbose_name=_("Network Profile")) + seg_id = tables.Column("seg_id", verbose_name=_("Segmentation ID")) + vlan_id = tables.Column("vlan_id", verbose_name=_("Vlan ID")) + result = tables.Column(get_result, verbose_name=_("Result")) + + def get_object_id(self, obj): + return obj.get('network_id') + + class Meta(object): + name = "networktable" + hidden_title = False + verbose_name = _("Networks") + table_actions = (SearchFilterAction,) + multi_select = False + + +class InstanceTable(tables.DataTable): + instance_name = tables.Column(get_instance_info, verbose_name=_("Name")) + host = tables.Column("host", verbose_name=_("Host")) + tor = tables.Column("remote_system_name", verbose_name=_("TOR")) + network_name = tables.Column("network_name", + verbose_name=_("Network Name")) + local_vlan = tables.Column("local_vlan", verbose_name=_("Local Vlan")) + vdp_vlan = tables.Column(get_vdp, verbose_name=_("Link Local Vlan")) + seg_id = tables.Column("seg_id", verbose_name=_("Segmentation ID")) + result = tables.Column(get_result, verbose_name=_("Result")) + + def get_object_id(self, obj): + return obj.get('port_id') + + class Meta(object): + name = "instancetable" + hidden_title = False + verbose_name = _("Instances") + table_actions = (SearchFilterAction,) + multi_select = False + + +class AgentsTable(tables.DataTable): + host = tables.Column("host", verbose_name=_("Host"), + link='horizon:cisco:dfa:detail') + created = tables.Column("created", verbose_name=_("Created")) + heartbeat = tables.Column("heartbeat", verbose_name=_("Heartbeat")) + agent_status = tables.Column("agent_status", + verbose_name=_("Agent Status")) + + def get_object_id(self, obj): + return obj.get('host') + + class Meta(object): + name = "agentstable" + verbose_name = _("Agents Table") + table_actions = (SearchFilterAction,) + multi_select = False + + +class TopologyTable(tables.DataTable): + interface = tables.Column("interface", verbose_name=_("Interface")) + remote_port = tables.Column("remote_port", verbose_name=_("Remote Port")) + bond_intf = tables.Column("bond_intf", verbose_name=_("Bond Interface")) + remote_evb_cfgd = tables.Column("remote_evb_cfgd", + verbose_name=_("Remote EVB Configured")) + remote_system_desc = tables.Column("remote_system_desc", + verbose_name=_("Remote System")) + remote_chassis_mac = tables.Column("remote_chassis_mac", + verbose_name=_("Remote Chassis Mac")) + remote_mgmt_addr = tables.Column("remote_mgmt_addr", + verbose_name=_("Remote Mgmt Address")) + remote_system_name = tables.Column("remote_system_name", + verbose_name=_("Remote Sys Name")) + remote_evb_mode = tables.Column("remote_evb_mode", + verbose_name=_("Remote EVB Mode")) + + def get_object_id(self, obj): + return obj.get('interface') + + class Meta(object): + name = "topology" + hidden_title = False + verbose_name = _("Topology") + multi_select = False diff --git a/horizon_cisco_ui/cisco/dfa/tabs.py b/horizon_cisco_ui/cisco/dfa/tabs.py new file mode 100644 index 0000000..9292a16 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/tabs.py @@ -0,0 +1,133 @@ +# 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 dateutil.parser +import json +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs +from horizon_cisco_ui.cisco.dfa import dfa_client +from horizon_cisco_ui.cisco.dfa import tables + +LOG = logging.getLogger(__name__) + + +class FabricSummaryTab(tabs.TableTab): + name = _("Fabric Summary") + slug = "fabric_summary_tab" + table_classes = (tables.FabricSummaryTable,) + template_name = ("horizon/common/_detail_table.html") + + def get_fabricsummary_data(self): + summary = [] + try: + summary = dfa_client.get_fabric_summary() + except Exception as exc: + exceptions.handle(self.request, exc.message) + return summary + + +class CFGProfileTab(tabs.TableTab): + name = _("Network Profiles") + slug = "cfgprofile_tab" + table_classes = (tables.CFGProfileTable,) + template_name = ("horizon/common/_detail_table.html") + + def get_cfgprofile_data(self): + try: + cfgplist = dfa_client.get_config_profiles_detail() + profile_list = [p for p in cfgplist + if (p.get('profileSubType') == + 'network:universal')] + except Exception as exc: + profile_list = [] + exceptions.handle(self.request, exc.message) + return profile_list + + +class NFEInfoTab(tabs.TableTab): + + name = _("Fabric View") + slug = "nfe_info_tab" + table_classes = (tables.ProjectTable, tables.NetworkTable, + tables.InstanceTable, ) + template_name = 'cisco/dfa/enablerinfo_tables.html' + + def get_projecttable_data(self): + try: + tenant = dict(tenant_id=self.request.user.project_id) + project_list = dfa_client.get_project_details(tenant) + except Exception as exc: + project_list = [] + exceptions.handle(self.request, exc.message) + return project_list + + def get_networktable_data(self): + try: + tenant = dict(tenant_id=self.request.user.project_id) + netlist = dfa_client.get_network_by_tenant_id(tenant) + except Exception as exc: + netlist = [] + exceptions.handle(self.request, exc.message) + return netlist + + def get_instancetable_data(self): + try: + tenant = dict(tenant_id=self.request.user.project_id) + instance_list = dfa_client.get_instance_by_tenant_id(tenant) + agent_list = dfa_client.get_agents_details() + port = {} + for agent in agent_list: + cfg = json.loads(agent.get('config')) + topo = cfg.get('topo') + intf = topo.get(cfg.get('veth_intf')) + host = agent.get('host') + if not intf: + continue + port.update({host: dict(veth_intf=cfg.get('uplink'), + remote_system_name=intf.get('remote_system_name'), + remote_port=intf.get('remote_port'))}) + if port: + for instance in instance_list: + instance.update(port.get(instance.get('host'))) + except Exception as exc: + instance_list = [] + exceptions.handle(self.request, exc.message) + return instance_list + + +class NFEAgentTab(tabs.TableTab): + name = _("Topology View") + slug = "nfe_agents_tab" + table_classes = (tables.AgentsTable,) + template_name = ("horizon/common/_detail_table.html") + + def get_agentstable_data(self): + try: + agent_list = dfa_client.get_agents_details() + for agent in agent_list: + agent["heartbeat"] = dateutil.parser.parse( + agent.get('heartbeat')) + agent["created"] = dateutil.parser.parse(agent.get('created')) + except Exception as exc: + agent_list = [] + exceptions.handle(self.request, exc.message) + return agent_list + + +class DFATabs(tabs.TabGroup): + slug = "dfa_tabs" + tabs = (FabricSummaryTab, CFGProfileTab, NFEInfoTab, NFEAgentTab) + sticky = True diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_associate.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_associate.html new file mode 100644 index 0000000..b3f6f26 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_associate.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Data Center Interconnect:" %} {% blocktrans %} Cisco DCI solutions extend LAN and SAN connectivity across geographically dispersed active data centers.{% endblocktrans %}

+

{% trans "DCI ID:" %} {% blocktrans %} 1 - 1600000{% endblocktrans %}

+{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_overview.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_overview.html new file mode 100644 index 0000000..ee4d636 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_overview.html @@ -0,0 +1,19 @@ +{% load i18n sizeformat parse_date %} + +
+
+
{% trans "Host Name" %}
+
{{ agent.host }}
+
{% trans "Created At" %}
+
{{ agent.created|parse_date }}
+
{% trans "Heartbeat" %}
+
{{ agent.heartbeat|parse_date }}
+
{% trans "Uplink" %}
+
{{ uplink |default:_("None")}}
+
{% trans "veth Interface" %}
+
{{ veth_intf |default:_("None")}}
+
{% trans "Member Ports" %}
+
{{ memb_ports |default:_("None")}}
+
+
+ diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_profile_overview.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_profile_overview.html new file mode 100644 index 0000000..5391443 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_detail_profile_overview.html @@ -0,0 +1,18 @@ +{% load i18n %} + +
+

{% trans Network Profile is an autoconfiguration template consisting of collection of commands which instantiates day-1 tenant-related configurations on CISCO Nexus switches.
Profiles can be added or modified only from DCNM %}


+
+
{% trans "Profile Name: " %}
+
{{ Profile_Name }}
+
{% trans "Profile Type: " %}
+
{{ Profile_Type }}
+
{% trans "Forwarding Mode: " %}
+
{{ fwding_mode }}
+
{% trans "Description: " %}
+
{{ description }}

+
{% trans "Commands: " %}
+
{% autoescape off %}{{ commands }}{% endautoescape %}
+
+
+ diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_disassociate.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_disassociate.html new file mode 100644 index 0000000..df14784 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_disassociate.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body%} +

{% blocktrans %}Are you sure to dis-associate DCI ID to Project?{% endblocktrans %}

+{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_instance.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_instance.html new file mode 100644 index 0000000..fd75895 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_instance.html @@ -0,0 +1,19 @@ +{% load i18n %} +{{ name }}{% endblocktrans %} + + diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_profile_overview.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_profile_overview.html new file mode 100644 index 0000000..f3dfefa --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_profile_overview.html @@ -0,0 +1,4 @@ +{% load i18n %} +{{ name }} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/_reason.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/_reason.html new file mode 100644 index 0000000..d9b5d05 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/_reason.html @@ -0,0 +1,14 @@ +{% load i18n %} +{{ result }}{% endblocktrans %} + diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/associate.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/associate.html new file mode 100644 index 0000000..88fc5f4 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/associate.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Associate DCI ID to Project" %}{% endblock %} + +{% block main %} + {% include "cisco/dfa/_associate.html" %} +{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/detail.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/detail.html new file mode 100644 index 0000000..3a151ce --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n breadcrumb_nav %} +{% block title %}{% trans "Fabric Enabler Hosts"%}{% endblock %} + +{% block main %} +
+
+ {% include "cisco/dfa/_detail_overview.html" %} +
+
+ {{ topology_table.render }} +
+
+
+{% endblock %} + diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/detailprofile.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/detailprofile.html new file mode 100644 index 0000000..5b0b95c --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/detailprofile.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n breadcrumb_nav %} +{% block title %}{% trans "Network Profiles"%}{% endblock %} + +{% block main %} +
+
+ {% include "cisco/dfa/_detail_profile_overview.html" %} +
+
+{% endblock %} + diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/disassociate.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/disassociate.html new file mode 100644 index 0000000..6be51e8 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/disassociate.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Disassociate DCI ID from Project" %}{% endblock %} + +{% block main %} + {% include "cisco/dfa/_disassociate.html" %} +{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/enablerinfo_tables.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/enablerinfo_tables.html new file mode 100644 index 0000000..909dab9 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/enablerinfo_tables.html @@ -0,0 +1,14 @@ +{% block main %} +
+ {{ projecttable_table.render }} +
+ +
+ {{ networktable_table.render }} +
+ +
+ {{ instancetable_table.render }} +
+ +{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/templates/dfa/index.html b/horizon_cisco_ui/cisco/dfa/templates/dfa/index.html new file mode 100644 index 0000000..bfb2bb3 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/templates/dfa/index.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %} {% trans "DFA" %} {% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/horizon_cisco_ui/cisco/dfa/test/dfa_client_test.py b/horizon_cisco_ui/cisco/dfa/test/dfa_client_test.py index 47a2864..657233a 100644 --- a/horizon_cisco_ui/cisco/dfa/test/dfa_client_test.py +++ b/horizon_cisco_ui/cisco/dfa/test/dfa_client_test.py @@ -1,3 +1,6 @@ +# Copyright 2014 Cisco Systems, Inc. +# All Rights Reserved. +# # 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 @@ -10,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import mock import platform @@ -38,12 +40,13 @@ class DFAClientTestCase(test.BaseAdminViewTests): name='net1', tenant_id=1) - message = json.dumps(network) - with mock.patch.object(self.client.clnt, 'call') as mock_call: - self.client.associate_profile_with_network(network) + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + dfa_client.associate_profile_with_network(network) - mock_call.assert_called_with({}, 'associate_profile_with_network', - msg=message) + mock_client.return_value.call.assert_called_with( + {}, 'associate_profile_with_network', msg=network) def test_associate_profile_with_network_rpc_exception(self): network = dict(id='1125-as45-afg5f-3457', @@ -51,10 +54,97 @@ class DFAClientTestCase(test.BaseAdminViewTests): name='net1', tenant_id=1) - with mock.patch.object(self.client.clnt, 'call', - side_effect=dfa_client.messaging.MessagingException), \ + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as mock_client, \ self.assertRaises(dfa_client.exceptions.NotAvailable) as cm: - self.client.associate_profile_with_network(network) + mock_client.return_value.call = mock.MagicMock( + side_effect=dfa_client.messaging.MessagingException) + dfa_client.associate_profile_with_network(network) self.assertEqual('RPC to Fabric Enabler failed', str(cm.exception)) + + def test_do_associate_dci_id_to_project(self): + tenant = dict(tenant_id=1, + tenant_name='Project1', + dci_id=1001) + + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.cast = mock.MagicMock() + dfa_client.do_associate_dci_id_to_project(tenant) + + mock_client.return_value.cast.assert_called_with( + {}, 'associate_dci_id_to_project', msg=tenant) + + def test_get_fabric_summary(self): + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_fabric_summary() + mock_client.return_value.call.assert_called_with( + {}, 'get_fabric_summary', msg={}) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_config_profiles_detail(self): + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_config_profiles_detail() + + mock_client.return_value.call.assert_called_with( + {}, 'get_config_profiles_detail', msg={}) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_project_details(self): + tenant = dict(tenant_id=1) + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_project_details(tenant) + + mock_client.return_value.call.assert_called_with( + {}, 'get_project_detail', msg=tenant) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_network_by_tenant_id(self): + tenant = dict(tenant_id=1) + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_network_by_tenant_id(tenant) + + mock_client.return_value.call.assert_called_with( + {}, 'get_all_networks_for_tenant', msg=tenant) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_instance_by_tenant_id(self): + tenant = dict(tenant_id=1) + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_instance_by_tenant_id(tenant) + + mock_client.return_value.call.assert_called_with( + {}, 'get_instance_by_tenant_id', msg=tenant) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_agents_details(self): + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_agents_details() + + mock_client.return_value.call.assert_called_with( + {}, 'get_agents_details', msg={}) + self.assertEqual(resp, mock_client.return_value.call.return_value) + + def test_get_agent_details_per_host(self): + agent = dict(tenant_id=1) + with mock.patch('horizon_cisco_ui.cisco.dfa.dfa_client.dfaclient') as \ + mock_client: + mock_client.return_value.call = mock.MagicMock() + resp = dfa_client.get_agent_details_per_host(agent) + + mock_client.return_value.call.assert_called_with( + {}, 'get_agent_details_per_host', msg=agent) + self.assertEqual(resp, mock_client.return_value.call.return_value) diff --git a/horizon_cisco_ui/cisco/dfa/test/dfa_horizon_test.py b/horizon_cisco_ui/cisco/dfa/test/dfa_horizon_test.py new file mode 100644 index 0000000..d846f5f --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/test/dfa_horizon_test.py @@ -0,0 +1,132 @@ +# 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.core.urlresolvers import reverse + +from horizon_cisco_ui.cisco.dfa.tabs import dfa_client as dc +from horizon_cisco_ui.cisco.dfa.test import test_data + +import mock +from openstack_dashboard.test import helpers as test + + +class DFATestCase(test.BaseAdminViewTests): + + def setUp(self): + super(DFATestCase, self).setUp() + self.dfa_client = dc.DFAClient + + def _setup_test_data(self): + super(DFATestCase, self)._setup_test_data() + test_data.data(self) + + def _test_base_index(self): + profiles = self.dfa_config_profile.list() + projects = self.dfa_project.list() + networks = self.dfa_network.list() + instances = self.dfa_instance.list() + agents = self.dfa_agent.list() + summary = self.dfa_summary.list()[0] + + with mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_config_profiles_detail', return_value=profiles), \ + mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_network_by_tenant_id', + return_value=networks), \ + mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_instance_by_tenant_id', + return_value=instances), \ + mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_project_details', + return_value=projects), \ + mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_fabric_summary', + return_value=summary), \ + mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_agents_details', + return_value=agents): + res = self.client.get(reverse('horizon:cisco:dfa:index')) + + self.assertTemplateUsed(res, 'cisco/dfa/index.html') + + return res + + def test_fabric_summary_tab_index(self): + res = self._test_base_index() + services_tab = res.context['tab_group'].get_tab('fabric_summary_tab') + self.assertEqual( + services_tab._tables['fabricsummary'].data, + [summary for summary in self.dfa_summary.list()[0]]) + + def test_cfg_profile_index(self): + res = self._test_base_index() + services_tab = res.context['tab_group'].get_tab('cfgprofile_tab') + self.assertEqual( + services_tab._tables['cfgprofile'].data, + [profiles for profiles in self.dfa_config_profile.list()]) + + def test_nfe_info_index(self): + res = self._test_base_index() + services_tab = res.context['tab_group'].get_tab('nfe_info_tab') + self.assertEqual( + services_tab._tables['projecttable'].data, + [project for project in self.dfa_project.list()]) + + self.assertEqual( + services_tab._tables['networktable'].data, + [network for network in self.dfa_network.list()]) + + def test_nfe_agents_index(self): + res = self._test_base_index() + services_tab = res.context['tab_group'].get_tab('nfe_agents_tab') + self.assertEqual( + services_tab._tables['agentstable'].data, + [agent for agent in self.dfa_agent.list()]) + + def test_agent_detail(self): + agent = self.dfa_agent.list() + with mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_agent_details_per_host', + return_value=agent): + res = self.client.get(reverse('horizon:cisco:dfa:detail', + args=['compute0'])) + self.assertTemplateUsed(res, 'cisco/dfa/detail.html') + + def test_agent_detail_exception(self): + with mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.get_agent_details_per_host', + side_effect=dc.exceptions.NotAvailable): + url = reverse('horizon:cisco:dfa:detail', args=['compute0']) + res = self.client.get(url) + + redir_url = reverse('horizon:cisco:dfa:index') + self.assertRedirectsNoFollow(res, redir_url) + + def test_associate_dci_to_project(self): + formdata = {'project_id': 123456, 'dci_id': 1001} + with mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.do_associate_dci_id_to_project'): + url = reverse('horizon:cisco:dfa:associate', args=['123456']) + res = self.client.post(url, formdata) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, '/cisco/dfa/') + + def test_disassociate_dci_to_project(self): + formdata = {'project_id': 123456, 'dci_id': 0} + with mock.patch('horizon_cisco_ui.cisco.dfa.tabs.dfa_client' + '.do_associate_dci_id_to_project'): + url = reverse('horizon:cisco:dfa:disassociate', args=['123456']) + res = self.client.post(url, formdata) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, '/cisco/dfa/') diff --git a/horizon_cisco_ui/cisco/dfa/test/test_data.py b/horizon_cisco_ui/cisco/dfa/test/test_data.py new file mode 100644 index 0000000..0524093 --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/test/test_data.py @@ -0,0 +1,98 @@ +# Copyright 2012 Nebula, 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 openstack_dashboard.test.test_data import utils + + +def data(TEST): + # Test DataContainers for DFA Workflow + TEST.dfa_precreate_network = utils.TestDataContainer() + TEST.dfa_config_profile = utils.TestDataContainer() + TEST.dfa_project = utils.TestDataContainer() + TEST.dfa_network = utils.TestDataContainer() + TEST.dfa_instance = utils.TestDataContainer() + TEST.dfa_agent = utils.TestDataContainer() + TEST.dfa_summary = utils.TestDataContainer() + + precreate_dict = {'tenant_id': '1', + 'nwk_name': 'net1', + 'subnet': '10.0.0.0/24', + 'cfgp': 'defaultNetworkL2Profile'} + TEST.dfa_precreate_network.add(precreate_dict) + + cfg_profile_dict = {'profileSubType': 'network:universal', + 'description': 'native dhcp EF Profile', + 'editable': 'yes', + 'configCommands': 'vlan $vlanId', + 'profileType': 'IPBD', + 'profileName': 'nativeDhcpEfProfile', + 'forwardingMode': 'proxy-gateway', + 'modifyTimestamp': 'Wed Mar 30 212631 PDT 2016'} + TEST.dfa_config_profile.add(cfg_profile_dict) + + project_dict = {'project_id': 'cc073fa35b544e27b6a7802e9919afb1', + 'dci_id': 12344, + 'project_name': 'Cisco', + 'result': 'SUCCESS'} + TEST.dfa_project.add(project_dict) + + network_dict = {'network_id': '05df940e-7a68-4ead-9618-ae199c4dc289', + 'reason': 'Request to DCNM failed: [500] Segment ID: \ + 76388 already exists.', + 'seg_id': 76388, + 'result': 'CREATE_FAIL', + 'config_profile': 'defaultNetworkL2Profile', + 'network_name': 'net2', + 'vlan_id': None} + TEST.dfa_network.add(network_dict) + + network_dict = {'network_id': '7c67bc14-8b7b-44ea-9ed5-c3fb36d7bfd9', + 'reason': None, + 'seg_id': 76377, + 'result': 'SUCCESS', + 'config_profile': 'defaultNetworkUniversalEfProfile', + 'network_name': 'network1', + 'vlan_id': 10} + TEST.dfa_network.add(network_dict) + + instance_dict = {'local_vlan': 10, + 'name': 'INS1', + 'network_id': '7c67bc14-8b7b-44ea-9ed5-c3fb36d7bfd9', + 'instance_id': 'e7cd03bed37b4de48f19fc5232056025', + 'host': 'ctayal-openstack', + 'veth_intf': 'eth1', + 'remote_system_name': 'N6k-75', + 'remote_port': 'Ethernet1/47', + 'seg_id': 76377, + 'result': 'SUCCESS', + 'vdp_vlan': 110, + 'port_id': '6672d670-9ba9-4889-8754-367bd51165fd'} + TEST.dfa_instance.add(instance_dict) + + agent_dict = {'heartbeat': '2016-09-01T04:40:17.000000', + 'host': 'compute0', + 'config': '{"memb_ports": null, "veth_intf": "", \ + "uplink": ""}', + 'created': u'2016-07-27T06:34:48.000000'} + TEST.dfa_agent.add(agent_dict) + + summary_dict = [{u'value': 2.0, u'key': u'Fabric Enabler Version'}, + {u'value': u'10.0(1)', u'key': u'DCNM Version'}, + {u'value': u'172.28.11.151', u'key': u'DCNM IP'}, + {u'value': u'n6k', u'key': u'Switch Type'}, + {u'value': u'fabricpath', u'key': u'Fabric Type'}, + {u'value': u'2', u'key': u'Fabric ID'}, + {u'value': u'76345-76545', u'key': u'Segment ID Range'}] + + TEST.dfa_summary.add(summary_dict) diff --git a/horizon_cisco_ui/cisco/dfa/urls.py b/horizon_cisco_ui/cisco/dfa/urls.py new file mode 100644 index 0000000..d8cd20d --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/urls.py @@ -0,0 +1,33 @@ +# 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 import patterns +from django.conf.urls import url + +from horizon_cisco_ui.cisco.dfa import views + +AGENTS = r'^(?P[^/]+)/%s$' +PROJECT = r'^(?P[^/]+)/%s$' +PROFILE = r'^(?P[^/]+)/%s$' + + +urlpatterns = patterns( + '', + url(r'^$', views.IndexView.as_view(), name='index'), + url(AGENTS % 'detail', views.DetailView.as_view(), name='detail'), + url(PROFILE % 'detailprofile', views.DetailProfileView.as_view(), + name='detailprofile'), + url(PROJECT % 'associate', views.AssociateDCIToProjectView.as_view(), + name='associate'), + url(PROJECT % 'disassociate', views.DisssociateDCIToProjectView.as_view(), + name='disassociate'), +) diff --git a/horizon_cisco_ui/cisco/dfa/views.py b/horizon_cisco_ui/cisco/dfa/views.py new file mode 100644 index 0000000..2de3c4b --- /dev/null +++ b/horizon_cisco_ui/cisco/dfa/views.py @@ -0,0 +1,191 @@ +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +import json +import logging +import re + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon import tabs +from horizon.utils import memoized +from horizon import views + +from horizon_cisco_ui.cisco.dfa import dfa_client +from horizon_cisco_ui.cisco.dfa import forms as dfaforms +from horizon_cisco_ui.cisco.dfa import tables as dfatables +from horizon_cisco_ui.cisco.dfa import tabs as dfatab + +LOG = logging.getLogger(__name__) + + +class IndexView(tabs.TabbedTableView): + tab_group_class = dfatab.DFATabs + template_name = 'cisco/dfa/index.html' + page_title = _("Programmable Fabric") + + +class AssociateDCIToProjectView(forms.ModalFormView): + form_class = dfaforms.AssociateDCI + form_id = "associate_form" + modal_header = _("Associate DCI ID to Project") + template_name = 'cisco/dfa/associate.html' + submit_label = _("ASSOCIATE") + submit_url = "horizon:cisco:dfa:associate" + success_url = reverse_lazy('horizon:cisco:dfa:index') + page_title = _("ASSOCIATE DCI ID") + + def get_context_data(self, **kwargs): + context = super(AssociateDCIToProjectView, + self).get_context_data(**kwargs) + args = (self.kwargs['project_id'],) + context["project_id"] = self.kwargs['project_id'] + context["submit_url"] = reverse(self.submit_url, args=args) + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + return self.kwargs["project_id"] + + def get_initial(self): + return {'project_id': self._get_object()} + + +class DisssociateDCIToProjectView(forms.ModalFormView): + form_class = dfaforms.DisassociateDCI + form_id = "disassociate_form" + modal_header = _("Disassociate DCI ID to Project") + template_name = 'cisco/dfa/disassociate.html' + submit_label = _("DISASSOCIATE") + submit_url = "horizon:cisco:dfa:disassociate" + success_url = reverse_lazy('horizon:cisco:dfa:index') + + def get_context_data(self, **kwargs): + context = super(DisssociateDCIToProjectView, + self).get_context_data(**kwargs) + args = (self.kwargs['project_id'],) + context["project_id"] = self.kwargs['project_id'] + context["submit_url"] = reverse(self.submit_url, args=args) + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + return self.kwargs["project_id"] + + def get_initial(self): + return {'project_id': self._get_object()} + + +class DetailProfileView(views.HorizonTemplateView): + template_name = 'cisco/dfa/detailprofile.html' + page_title = "{{ profile_name }}" + + def get_context_data(self, **kwargs): + context = super(DetailProfileView, self).get_context_data(**kwargs) + data = self._get_data() + rep = {'\r': '
', ' ': ' '} + rep = dict((re.escape(k), v) for k, v in rep.iteritems()) + pattern = re.compile("|".join(rep.keys())) + commands = pattern.sub(lambda m: rep[re.escape(m.group(0))], + data.get('configCommands')) + context['Profile_Name'] = self.kwargs['profile_name'].split(":")[0] + context['Profile_Type'] = self.kwargs['profile_name'].split(":")[1] + context['fwding_mode'] = data.get('forwardingMode') + context['description'] = data.get('description') + context['commands'] = commands + + return context + + @memoized.memoized_method + def _get_data(self): + try: + profile_name = self.kwargs['profile_name'] + cfg_profile = {'profile': profile_name.split(":")[0], + 'ftype': profile_name.split(":")[1]} + data = dfa_client.get_per_config_profile_detail(cfg_profile) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve host details.'), + redirect=self.get_redirect_url()) + return data + + @staticmethod + def get_redirect_url(): + return reverse_lazy('horizon:cisco:dfa:index') + + +class DetailView(tables.MultiTableView): + table_classes = (dfatables.TopologyTable, ) + template_name = 'cisco/dfa/detail.html' + page_title = "{{ agent.host }}" + + def get_topology_data(self): + try: + topology = [] + agent = self._get_data() + cfg = json.loads(agent.get('config')) + topo = cfg.get('topo') + for key in topo.keys(): + intf = topo.get(key) + if not intf.get('remote_evb_cfgd'): + continue + topology.append( + dict( + interface=cfg.get('uplink'), + remote_port=intf.get('remote_port'), + bond_intf=intf.get('bond_intf'), + remote_port_mac=intf.get('remote_port_id_mac'), + remote_evb_cfgd=intf.get('remote_evb_cfgd'), + remote_system_desc=intf.get('remote_system_desc'), + remote_chassis_mac=intf.get('remote_chassis_id_mac'), + remote_mgmt_addr=intf.get('remote_mgmt_addr'), + remote_system_name=intf.get('remote_system_name'), + remote_evb_mode=intf.get('remote_evb_mode'))) + except Exception: + topology = [] + msg = _('Neighborship is not established for this server') + exceptions.handle(self.request, msg) + return topology + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + agent = self._get_data() + context["agent"] = agent + cfg = json.loads(agent.get('config')) + context["uplink"] = cfg.get('uplink') + context["memb_ports"] = cfg.get('memb_ports') + context["veth_intf"] = cfg.get('veth_intf') + context["url"] = self.get_redirect_url() + return context + + @memoized.memoized_method + def _get_data(self): + try: + host_name = self.kwargs['host'] + NFEhost = dict(host=host_name) + agent = (dfa_client.get_agent_details_per_host(NFEhost))[0] + agent["heartbeat"] = agent.get('heartbeat').replace('T', ' ')[:-7] + agent["created"] = agent.get('created').replace('T', ' ')[:-7] + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve host details.'), + redirect=self.get_redirect_url()) + return agent + + @staticmethod + def get_redirect_url(): + return reverse_lazy('horizon:cisco:dfa:index') diff --git a/horizon_cisco_ui/cisco/dfa/workflows.py b/horizon_cisco_ui/cisco/dfa/workflows.py index 19b6ea6..aa82881 100644 --- a/horizon_cisco_ui/cisco/dfa/workflows.py +++ b/horizon_cisco_ui/cisco/dfa/workflows.py @@ -43,10 +43,7 @@ class DFAConfigProfileAction(workflows.Action): def _get_cfg_profiles(self, request): profiles = [] try: - dfaclient = dfa_client.DFAClient() - if not bool(dfaclient.__dict__): - return profiles - cfgplist = dfaclient.get_config_profiles_detail() + cfgplist = dfa_client.get_config_profiles_detail() profiles = [q for p in cfgplist if (p.get('profileSubType') == 'network:universal') for q in [p.get('profileName')]] @@ -89,12 +86,11 @@ class DFACreateNetwork(upstream_networks_workflows.CreateNetwork): if not network: return False if data['cfg_profile']: - dfaclient = dfa_client.DFAClient() associate_data = {'id': network['id'], 'cfgp': data['cfg_profile'], 'name': network['name'], 'tenant_id': request.user.project_id} - dfaclient.associate_profile_with_network(associate_data) + dfa_client.associate_profile_with_network(associate_data) # If we do not need to create a subnet, return here. if not data['with_subnet']: return True diff --git a/horizon_cisco_ui/enabled/_6020_dfa.py b/horizon_cisco_ui/enabled/_6020_dfa.py new file mode 100644 index 0000000..c7cfaa2 --- /dev/null +++ b/horizon_cisco_ui/enabled/_6020_dfa.py @@ -0,0 +1,5 @@ +PANEL = 'dfa' +PANEL_GROUP = 'default' +PANEL_DASHBOARD = 'cisco' + +ADD_PANEL = 'horizon_cisco_ui.cisco.dfa.panel.DFA'