Merge branch 'develop' into pygui-rmallservices-fix
This commit is contained in:
commit
b116d525d9
7 changed files with 248 additions and 34 deletions
|
@ -357,6 +357,8 @@ class Session:
|
||||||
)
|
)
|
||||||
interface = create_interface(node_one, net_one, interface_one)
|
interface = create_interface(node_one, net_one, interface_one)
|
||||||
node_one_interface = interface
|
node_one_interface = interface
|
||||||
|
wireless_net = isinstance(net_one, (EmaneNet, WlanNode))
|
||||||
|
if not wireless_net:
|
||||||
link_config(net_one, interface, link_options)
|
link_config(net_one, interface, link_options)
|
||||||
|
|
||||||
# network to node
|
# network to node
|
||||||
|
@ -368,7 +370,8 @@ class Session:
|
||||||
)
|
)
|
||||||
interface = create_interface(node_two, net_one, interface_two)
|
interface = create_interface(node_two, net_one, interface_two)
|
||||||
node_two_interface = interface
|
node_two_interface = interface
|
||||||
if not link_options.unidirectional:
|
wireless_net = isinstance(net_one, (EmaneNet, WlanNode))
|
||||||
|
if not link_options.unidirectional and not wireless_net:
|
||||||
link_config(net_one, interface, link_options)
|
link_config(net_one, interface, link_options)
|
||||||
|
|
||||||
# network to network
|
# network to network
|
||||||
|
|
158
daemon/core/gui/dialogs/find.py
Normal file
158
daemon/core/gui/dialogs/find.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
|
||||||
|
|
||||||
|
class FindDialog(Dialog):
|
||||||
|
def __init__(self, master, app) -> None:
|
||||||
|
super().__init__(master, app, "Find", modal=True)
|
||||||
|
|
||||||
|
self.find_text = tk.StringVar(value="")
|
||||||
|
self.tree = None
|
||||||
|
self.draw()
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.close_dialog)
|
||||||
|
self.bind("<Return>", self.find_node)
|
||||||
|
|
||||||
|
def draw(self) -> None:
|
||||||
|
self.top.columnconfigure(0, weight=1)
|
||||||
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
self.top.rowconfigure(1, weight=5)
|
||||||
|
self.top.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
|
# Find node frame
|
||||||
|
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||||
|
frame.grid(sticky="nsew")
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
label = ttk.Label(frame, text="Find:")
|
||||||
|
label.grid()
|
||||||
|
entry = ttk.Entry(frame, textvariable=self.find_text)
|
||||||
|
entry.grid(row=0, column=1, sticky="nsew")
|
||||||
|
|
||||||
|
# node list frame
|
||||||
|
frame = ttk.Frame(self.top)
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.rowconfigure(0, weight=1)
|
||||||
|
frame.grid(sticky="nsew", padx=PADX, pady=PADY)
|
||||||
|
self.tree = ttk.Treeview(
|
||||||
|
frame,
|
||||||
|
columns=("nodeid", "name", "location", "detail"),
|
||||||
|
show="headings",
|
||||||
|
selectmode=tk.BROWSE,
|
||||||
|
)
|
||||||
|
self.tree.grid(sticky="nsew", pady=PADY)
|
||||||
|
style = ttk.Style()
|
||||||
|
heading_size = int(self.app.guiconfig["scale"] * 10)
|
||||||
|
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
|
||||||
|
self.tree.column("nodeid", stretch=tk.YES, anchor="center")
|
||||||
|
self.tree.heading("nodeid", text="Node ID")
|
||||||
|
self.tree.column("name", stretch=tk.YES, anchor="center")
|
||||||
|
self.tree.heading("name", text="Name")
|
||||||
|
self.tree.column("location", stretch=tk.YES, anchor="center")
|
||||||
|
self.tree.heading("location", text="Location")
|
||||||
|
self.tree.column("detail", stretch=tk.YES, anchor="center")
|
||||||
|
self.tree.heading("detail", text="Detail")
|
||||||
|
|
||||||
|
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||||
|
|
||||||
|
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||||
|
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||||
|
|
||||||
|
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||||
|
xscrollbar.grid(row=1, sticky="ew")
|
||||||
|
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||||
|
|
||||||
|
# button frame
|
||||||
|
frame = ttk.Frame(self.top)
|
||||||
|
frame.grid(sticky="nsew")
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
button = ttk.Button(frame, text="Find", command=self.find_node)
|
||||||
|
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||||
|
button = ttk.Button(frame, text="Cancel", command=self.close_dialog)
|
||||||
|
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
|
def clear_treeview_items(self) -> None:
|
||||||
|
"""
|
||||||
|
clear all items in the treeview
|
||||||
|
"""
|
||||||
|
for i in list(self.tree.get_children("")):
|
||||||
|
self.tree.delete(i)
|
||||||
|
|
||||||
|
def find_node(self, _event: tk.Event = None) -> None:
|
||||||
|
"""
|
||||||
|
Query nodes that have the same node name as our search key,
|
||||||
|
display results to tree view
|
||||||
|
"""
|
||||||
|
node_name = self.find_text.get().strip()
|
||||||
|
self.clear_treeview_items()
|
||||||
|
for node_id, node in sorted(
|
||||||
|
self.app.core.canvas_nodes.items(), key=lambda x: x[0]
|
||||||
|
):
|
||||||
|
name = node.core_node.name
|
||||||
|
if not node_name or node_name == name:
|
||||||
|
pos_x = round(node.core_node.position.x, 1)
|
||||||
|
pos_y = round(node.core_node.position.y, 1)
|
||||||
|
# TODO I am not sure what to insert for Detail column, leaving in blank for now
|
||||||
|
self.tree.insert(
|
||||||
|
"",
|
||||||
|
tk.END,
|
||||||
|
text=str(node_id),
|
||||||
|
values=(node_id, name, f"<{pos_x}, {pos_y}>", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
results = self.tree.get_children("")
|
||||||
|
if results:
|
||||||
|
self.tree.selection_set(results[0])
|
||||||
|
|
||||||
|
def close_dialog(self) -> None:
|
||||||
|
self.app.canvas.delete("find")
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def click_select(self, _event: tk.Event = None) -> None:
|
||||||
|
"""
|
||||||
|
find the node that matches search criteria, circle around that node
|
||||||
|
and scroll the x and y scrollbar to be able to see the node if
|
||||||
|
it is out of sight
|
||||||
|
"""
|
||||||
|
item = self.tree.selection()
|
||||||
|
if item:
|
||||||
|
self.app.canvas.delete("find")
|
||||||
|
node_id = int(self.tree.item(item, "text"))
|
||||||
|
canvas_node = self.app.core.canvas_nodes[node_id]
|
||||||
|
|
||||||
|
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
|
||||||
|
dist = 5 * self.app.guiconfig["scale"]
|
||||||
|
self.app.canvas.create_oval(
|
||||||
|
x0 - dist,
|
||||||
|
y0 - dist,
|
||||||
|
x1 + dist,
|
||||||
|
y1 + dist,
|
||||||
|
tags="find",
|
||||||
|
outline="red",
|
||||||
|
width=3.0 * self.app.guiconfig["scale"],
|
||||||
|
)
|
||||||
|
|
||||||
|
_x, _y, _, _ = self.app.canvas.bbox(canvas_node.id)
|
||||||
|
oid = self.app.canvas.find_withtag("rectangle")
|
||||||
|
x0, y0, x1, y1 = self.app.canvas.bbox(oid[0])
|
||||||
|
logging.debug("Dist to most left: %s", abs(x0 - _x))
|
||||||
|
logging.debug("White canvas width: %s", abs(x0 - x1))
|
||||||
|
|
||||||
|
# calculate the node's location
|
||||||
|
# (as fractions of white canvas's width and height)
|
||||||
|
# and instantly scroll the x and y scrollbar to that location
|
||||||
|
xscroll_fraction = abs(x0 - _x) / abs(x0 - x1)
|
||||||
|
yscroll_fraction = abs(y0 - _y) / abs(y0 - y1)
|
||||||
|
# scroll a little more to the left or a little bit up so that the node
|
||||||
|
# doesn't always fall in the most top-left corner
|
||||||
|
for i in range(2):
|
||||||
|
if xscroll_fraction > 0.05:
|
||||||
|
xscroll_fraction = xscroll_fraction - 0.05
|
||||||
|
if yscroll_fraction > 0.05:
|
||||||
|
yscroll_fraction = yscroll_fraction - 0.05
|
||||||
|
self.app.canvas.xview_moveto(xscroll_fraction)
|
||||||
|
self.app.canvas.yview_moveto(yscroll_fraction)
|
|
@ -72,12 +72,15 @@ class SessionsDialog(Dialog):
|
||||||
show="headings",
|
show="headings",
|
||||||
selectmode=tk.BROWSE,
|
selectmode=tk.BROWSE,
|
||||||
)
|
)
|
||||||
|
style = ttk.Style()
|
||||||
|
heading_size = int(self.app.guiconfig["scale"] * 10)
|
||||||
|
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
|
||||||
self.tree.grid(sticky="nsew")
|
self.tree.grid(sticky="nsew")
|
||||||
self.tree.column("id", stretch=tk.YES)
|
self.tree.column("id", stretch=tk.YES, anchor="center")
|
||||||
self.tree.heading("id", text="ID")
|
self.tree.heading("id", text="ID")
|
||||||
self.tree.column("state", stretch=tk.YES)
|
self.tree.column("state", stretch=tk.YES, anchor="center")
|
||||||
self.tree.heading("state", text="State")
|
self.tree.heading("state", text="State")
|
||||||
self.tree.column("nodes", stretch=tk.YES)
|
self.tree.column("nodes", stretch=tk.YES, anchor="center")
|
||||||
self.tree.heading("nodes", text="Node Count")
|
self.tree.heading("nodes", text="Node Count")
|
||||||
|
|
||||||
for index, session in enumerate(self.sessions):
|
for index, session in enumerate(self.sessions):
|
||||||
|
@ -213,3 +216,5 @@ class SessionsDialog(Dialog):
|
||||||
def on_closing(self) -> None:
|
def on_closing(self) -> None:
|
||||||
if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self):
|
if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self):
|
||||||
self.click_exit()
|
self.click_exit()
|
||||||
|
if not self.is_start_app:
|
||||||
|
self.destroy()
|
||||||
|
|
|
@ -11,6 +11,7 @@ from core.gui.dialogs.about import AboutDialog
|
||||||
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
||||||
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
|
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
|
||||||
from core.gui.dialogs.executepython import ExecutePythonDialog
|
from core.gui.dialogs.executepython import ExecutePythonDialog
|
||||||
|
from core.gui.dialogs.find import FindDialog
|
||||||
from core.gui.dialogs.hooks import HooksDialog
|
from core.gui.dialogs.hooks import HooksDialog
|
||||||
from core.gui.dialogs.ipdialog import IpConfigDialog
|
from core.gui.dialogs.ipdialog import IpConfigDialog
|
||||||
from core.gui.dialogs.macdialog import MacConfigDialog
|
from core.gui.dialogs.macdialog import MacConfigDialog
|
||||||
|
@ -114,6 +115,7 @@ class Menubar(tk.Menu):
|
||||||
Create edit menu
|
Create edit menu
|
||||||
"""
|
"""
|
||||||
menu = tk.Menu(self)
|
menu = tk.Menu(self)
|
||||||
|
menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find)
|
||||||
menu.add_command(label="Preferences", command=self.click_preferences)
|
menu.add_command(label="Preferences", command=self.click_preferences)
|
||||||
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
|
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
|
||||||
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
|
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
|
||||||
|
@ -125,6 +127,7 @@ class Menubar(tk.Menu):
|
||||||
label="Delete", accelerator="Ctrl+D", command=self.click_delete
|
label="Delete", accelerator="Ctrl+D", command=self.click_delete
|
||||||
)
|
)
|
||||||
self.add_cascade(label="Edit", menu=menu)
|
self.add_cascade(label="Edit", menu=menu)
|
||||||
|
self.app.master.bind_all("<Control-f>", self.click_find)
|
||||||
self.app.master.bind_all("<Control-x>", self.click_cut)
|
self.app.master.bind_all("<Control-x>", self.click_cut)
|
||||||
self.app.master.bind_all("<Control-c>", self.click_copy)
|
self.app.master.bind_all("<Control-c>", self.click_copy)
|
||||||
self.app.master.bind_all("<Control-v>", self.click_paste)
|
self.app.master.bind_all("<Control-v>", self.click_paste)
|
||||||
|
@ -397,6 +400,10 @@ class Menubar(tk.Menu):
|
||||||
self.core.create_new_session()
|
self.core.create_new_session()
|
||||||
self.core.xml_file = None
|
self.core.xml_file = None
|
||||||
|
|
||||||
|
def click_find(self, _event: tk.Event = None) -> None:
|
||||||
|
dialog = FindDialog(self.app, self.app)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
def click_preferences(self) -> None:
|
def click_preferences(self) -> None:
|
||||||
dialog = PreferencesDialog(self.app, self.app)
|
dialog = PreferencesDialog(self.app, self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
|
@ -260,7 +260,7 @@ class QuaggaService(CoreService):
|
||||||
if netaddr.valid_ipv4(a):
|
if netaddr.valid_ipv4(a):
|
||||||
return a
|
return a
|
||||||
# raise ValueError, "no IPv4 address found for router ID"
|
# raise ValueError, "no IPv4 address found for router ID"
|
||||||
return "0.0.0.0"
|
return "0.0.0.%d" % node.id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rj45check(ifc):
|
def rj45check(ifc):
|
||||||
|
@ -348,7 +348,19 @@ class Ospfv2(QuaggaService):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generatequaggaifcconfig(cls, node, ifc):
|
def generatequaggaifcconfig(cls, node, ifc):
|
||||||
return cls.mtucheck(ifc)
|
cfg = cls.mtucheck(ifc)
|
||||||
|
# external RJ45 connections will use default OSPF timers
|
||||||
|
if cls.rj45check(ifc):
|
||||||
|
return cfg
|
||||||
|
cfg += cls.ptpcheck(ifc)
|
||||||
|
return (
|
||||||
|
cfg
|
||||||
|
+ """\
|
||||||
|
ip ospf hello-interval 2
|
||||||
|
ip ospf dead-interval 6
|
||||||
|
ip ospf retransmit-interval 5
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Ospfv3(QuaggaService):
|
class Ospfv3(QuaggaService):
|
||||||
|
|
|
@ -40,10 +40,15 @@ class OvsService(SdnService):
|
||||||
|
|
||||||
cfg = "#!/bin/sh\n"
|
cfg = "#!/bin/sh\n"
|
||||||
cfg += "# auto-generated by OvsService (OvsService.py)\n"
|
cfg += "# auto-generated by OvsService (OvsService.py)\n"
|
||||||
cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n"
|
cfg += "## First make sure that the ovs services are up and running\n"
|
||||||
|
cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n\n"
|
||||||
|
cfg += "## create the switch itself, set the fail mode to secure, \n"
|
||||||
|
cfg += "## this stops it from routing traffic without defined flows.\n"
|
||||||
|
cfg += "## remove the -- and everything after if you want it to act as a regular switch\n"
|
||||||
cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n"
|
cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n"
|
||||||
cfg += "ip link set dev ovsbr0 up\n"
|
|
||||||
|
|
||||||
|
cfg += "\n## Now add all our interfaces as ports to the switch\n"
|
||||||
|
portnum = 1
|
||||||
for ifc in node.netifs():
|
for ifc in node.netifs():
|
||||||
if hasattr(ifc, "control") and ifc.control is True:
|
if hasattr(ifc, "control") and ifc.control is True:
|
||||||
continue
|
continue
|
||||||
|
@ -51,9 +56,8 @@ class OvsService(SdnService):
|
||||||
ifnum = ifnumstr[0]
|
ifnum = ifnumstr[0]
|
||||||
|
|
||||||
# create virtual interfaces
|
# create virtual interfaces
|
||||||
|
cfg += "## Create a veth pair to send the data to\n"
|
||||||
cfg += "ip link add rtr%s type veth peer name sw%s\n" % (ifnum, ifnum)
|
cfg += "ip link add rtr%s type veth peer name sw%s\n" % (ifnum, ifnum)
|
||||||
cfg += "ip link set dev rtr%s up\n" % ifnum
|
|
||||||
cfg += "ip link set dev sw%s up\n" % ifnum
|
|
||||||
|
|
||||||
# remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces
|
# remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces
|
||||||
# or assign them manually to rtr interfaces if zebra is not running
|
# or assign them manually to rtr interfaces if zebra is not running
|
||||||
|
@ -71,17 +75,37 @@ class OvsService(SdnService):
|
||||||
raise ValueError("invalid address: %s" % ifcaddr)
|
raise ValueError("invalid address: %s" % ifcaddr)
|
||||||
|
|
||||||
# add interfaces to bridge
|
# add interfaces to bridge
|
||||||
cfg += "ovs-vsctl add-port ovsbr0 eth%s\n" % ifnum
|
# Make port numbers explicit so they're easier to follow in reading the script
|
||||||
cfg += "ovs-vsctl add-port ovsbr0 sw%s\n" % ifnum
|
cfg += "## Add the CORE interface to the switch\n"
|
||||||
|
cfg += (
|
||||||
|
"ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n"
|
||||||
|
% (ifnum, ifnum, portnum)
|
||||||
|
)
|
||||||
|
cfg += "## And then add its sibling veth interface\n"
|
||||||
|
cfg += (
|
||||||
|
"ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n"
|
||||||
|
% (ifnum, ifnum, portnum + 1)
|
||||||
|
)
|
||||||
|
cfg += "## start them up so we can send/receive data\n"
|
||||||
|
cfg += "ovs-ofctl mod-port ovsbr0 eth%s up\n" % ifnum
|
||||||
|
cfg += "ovs-ofctl mod-port ovsbr0 sw%s up\n" % ifnum
|
||||||
|
cfg += "## Bring up the lower part of the veth pair\n"
|
||||||
|
cfg += "ip link set dev rtr%s up\n" % ifnum
|
||||||
|
portnum += 2
|
||||||
|
|
||||||
# Add rule for default controller if there is one local (even if the controller is not local, it finds it)
|
# Add rule for default controller if there is one local (even if the controller is not local, it finds it)
|
||||||
|
cfg += "\n## We assume there will be an SDN controller on the other end of this, \n"
|
||||||
|
cfg += "## but it will still function if there's not\n"
|
||||||
cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n"
|
cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n"
|
||||||
|
|
||||||
|
cfg += "\n## Now to create some default flows, \n"
|
||||||
|
cfg += "## if the above controller will be present then you probably want to delete them\n"
|
||||||
# Setup default flows
|
# Setup default flows
|
||||||
portnum = 1
|
portnum = 1
|
||||||
for ifc in node.netifs():
|
for ifc in node.netifs():
|
||||||
if hasattr(ifc, "control") and ifc.control is True:
|
if hasattr(ifc, "control") and ifc.control is True:
|
||||||
continue
|
continue
|
||||||
|
cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n"
|
||||||
cfg += (
|
cfg += (
|
||||||
"ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n"
|
"ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n"
|
||||||
% (portnum, portnum + 1)
|
% (portnum, portnum + 1)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from core.errors import CoreXmlError
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
||||||
from core.nodes.docker import DockerNode
|
from core.nodes.docker import DockerNode
|
||||||
from core.nodes.lxd import LxcNode
|
from core.nodes.lxd import LxcNode
|
||||||
from core.nodes.network import CtrlNet
|
from core.nodes.network import CtrlNet, WlanNode
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -559,7 +559,12 @@ class CoreXmlWriter:
|
||||||
)
|
)
|
||||||
link_element.append(interface_two)
|
link_element.append(interface_two)
|
||||||
|
|
||||||
# check for options
|
# check for options, don't write for emane/wlan links
|
||||||
|
node_one = self.session.get_node(link_data.node1_id)
|
||||||
|
node_two = self.session.get_node(link_data.node2_id)
|
||||||
|
is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet))
|
||||||
|
is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet))
|
||||||
|
if not any([is_node_one_wireless, is_node_two_wireless]):
|
||||||
options = etree.Element("options")
|
options = etree.Element("options")
|
||||||
add_attribute(options, "delay", link_data.delay)
|
add_attribute(options, "delay", link_data.delay)
|
||||||
add_attribute(options, "bandwidth", link_data.bandwidth)
|
add_attribute(options, "bandwidth", link_data.bandwidth)
|
||||||
|
|
Loading…
Reference in a new issue