Merge pull request #548 from coreemu/develop

merging 7.4.0
This commit is contained in:
bharnden 2021-01-11 09:10:57 -08:00 committed by GitHub
commit d98a9a5a91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 247 additions and 122 deletions

View file

@ -1,3 +1,26 @@
## 2021-01-11 CORE 7.4.0
* Installation
* fixed issue for automated install assuming ID_LIKE is always present in /etc/os-release
* gRPC API
* fixed issue stopping session and not properly going to data collect state
* fixed issue to have start session properly create a directory before configuration state
* core-pygui
* fixed issue handling deletion of wired link to a switch
* avoid saving edge metadata to xml when values are default
* fixed issue editing node mac addresses
* added support for configuring interface names
* fixed issue with potential node names to allow hyphens and remove under bars
* \#531 - fixed issue changing distributed nodes back to local
* core-daemon
* fixed issue to properly handle deleting links from a network to network node
* updated xml to support writing and reading link buffer configurations
* reverted change and removed mac learning from wlan, due to promiscuous like behavior
* fixed issue creating control interfaces when starting services
* fixed deadlock issue when clearing a session using sdt
* \#116 - fixed issue for wlans handling multiple mobility scripts at once
* \#539 - fixed issue in udp tlv api
## 2020-12-02 CORE 7.3.0 ## 2020-12-02 CORE 7.3.0
* core-daemon * core-daemon

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 7.3.0) AC_INIT(core, 7.4.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -221,9 +221,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
# clear previous state and setup for creation # clear previous state and setup for creation
session.clear() session.clear()
session.set_state(EventTypes.CONFIGURATION_STATE)
if not os.path.exists(session.session_dir): if not os.path.exists(session.session_dir):
os.mkdir(session.session_dir) os.mkdir(session.session_dir)
session.set_state(EventTypes.CONFIGURATION_STATE)
# location # location
if request.HasField("location"): if request.HasField("location"):
@ -315,6 +315,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("stop session: %s", request) logging.debug("stop session: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
session.data_collect()
session.shutdown() session.shutdown()
return core_pb2.StopSessionResponse(result=True) return core_pb2.StopSessionResponse(result=True)

View file

@ -2029,7 +2029,7 @@ class CoreUdpHandler(CoreHandler):
for session_id in sessions: for session_id in sessions:
session = self.server.mainserver.coreemu.sessions.get(session_id) session = self.server.mainserver.coreemu.sessions.get(session_id)
if session: if session:
logging.debug("session handling message: %s", session.session_id) logging.debug("session handling message: %s", session.id)
self.session = session self.session = session
self.handle_message(message) self.handle_message(message)
self.broadcast(message) self.broadcast(message)

View file

@ -12,7 +12,6 @@ from core.emulator.data import LinkOptions
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
@ -119,13 +118,12 @@ class EmaneModel(WirelessModel):
""" """
logging.debug("emane model(%s) has no post setup tasks", self.name) logging.debug("emane model(%s) has no post setup tasks", self.name)
def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: List[CoreInterface]) -> None:
""" """
Invoked from MobilityModel when nodes are moved; this causes Invoked from MobilityModel when nodes are moved; this causes
emane location events to be generated for the nodes in the moved emane location events to be generated for the nodes in the moved
list, making EmaneModels compatible with Ns2ScriptedMobility. list, making EmaneModels compatible with Ns2ScriptedMobility.
:param moved: moved nodes
:param moved_ifaces: interfaces that were moved :param moved_ifaces: interfaces that were moved
:return: nothing :return: nothing
""" """

View file

@ -144,6 +144,7 @@ class CoreEmu:
result = False result = False
if session: if session:
logging.info("shutting session down: %s", _id) logging.info("shutting session down: %s", _id)
session.data_collect()
session.shutdown() session.shutdown()
result = True result = True
else: else:

View file

@ -106,6 +106,9 @@ class EventTypes(Enum):
def should_start(self) -> bool: def should_start(self) -> bool:
return self.value > self.DEFINITION_STATE.value return self.value > self.DEFINITION_STATE.value
def already_collected(self) -> bool:
return self.value >= self.DATACOLLECT_STATE.value
class ExceptionLevels(Enum): class ExceptionLevels(Enum):
""" """

View file

@ -13,7 +13,7 @@ import tempfile
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
from core import constants, utils from core import constants, utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
@ -369,6 +369,19 @@ class Session:
node1.delete_iface(iface1_id) node1.delete_iface(iface1_id)
elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
node2.delete_iface(iface2_id) node2.delete_iface(iface2_id)
elif isinstance(node1, CoreNetworkBase) and isinstance(
node2, CoreNetworkBase
):
for iface in node1.get_ifaces(control=False):
if iface.othernet == node2:
node1.detach(iface)
iface.shutdown()
break
for iface in node2.get_ifaces(control=False):
if iface.othernet == node1:
node2.detach(iface)
iface.shutdown()
break
self.sdt.delete_link(node1_id, node2_id) self.sdt.delete_link(node1_id, node2_id)
def update_link( def update_link(
@ -558,11 +571,11 @@ class Session:
if isinstance(node, WlanNode): if isinstance(node, WlanNode):
self.mobility.set_model_config(_id, BasicRangeModel.name) self.mobility.set_model_config(_id, BasicRangeModel.name)
# boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes # boot nodes after runtime CoreNodes and PhysicalNodes
is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) is_boot_node = isinstance(node, (CoreNode, PhysicalNode))
if self.state == EventTypes.RUNTIME_STATE and is_boot_node: if self.state == EventTypes.RUNTIME_STATE and is_boot_node:
self.write_nodes() self.write_nodes()
self.add_remove_control_iface(node=node, remove=False) self.add_remove_control_iface(node, remove=False)
self.services.boot_services(node) self.services.boot_services(node)
self.sdt.add_node(node) self.sdt.add_node(node)
@ -757,9 +770,10 @@ class Session:
""" """
Shutdown all session nodes and remove the session directory. Shutdown all session nodes and remove the session directory.
""" """
if self.state == EventTypes.SHUTDOWN_STATE:
logging.info("session(%s) state(%s) already shutdown", self.id, self.state)
return
logging.info("session(%s) state(%s) shutting down", self.id, self.state) logging.info("session(%s) state(%s) shutting down", self.id, self.state)
if self.state != EventTypes.SHUTDOWN_STATE:
self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True)
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
# clear out current core session # clear out current core session
self.clear() self.clear()
@ -1115,13 +1129,16 @@ class Session:
""" """
Clear the nodes dictionary, and call shutdown for each node. Clear the nodes dictionary, and call shutdown for each node.
""" """
nodes_ids = []
with self.nodes_lock: with self.nodes_lock:
funcs = [] funcs = []
while self.nodes: while self.nodes:
_, node = self.nodes.popitem() _, node = self.nodes.popitem()
self.sdt.delete_node(node.id) nodes_ids.append(node.id)
funcs.append((node.shutdown, [], {})) funcs.append((node.shutdown, [], {}))
utils.threadpool(funcs) utils.threadpool(funcs)
for node_id in nodes_ids:
self.sdt.delete_node(node_id)
def write_nodes(self) -> None: def write_nodes(self) -> None:
""" """
@ -1245,6 +1262,14 @@ class Session:
:return: nothing :return: nothing
""" """
if self.state.already_collected():
logging.info(
"session(%s) state(%s) already data collected", self.id, self.state
)
return
logging.info("session(%s) state(%s) data collection", self.id, self.state)
self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True)
# stop event loop # stop event loop
self.event_loop.stop() self.event_loop.stop()
@ -1266,10 +1291,8 @@ class Session:
self.update_control_iface_hosts(remove=True) self.update_control_iface_hosts(remove=True)
# remove all four possible control networks # remove all four possible control networks
self.add_remove_control_net(0, remove=True) for i in range(4):
self.add_remove_control_net(1, remove=True) self.add_remove_control_net(i, remove=True)
self.add_remove_control_net(2, remove=True)
self.add_remove_control_net(3, remove=True)
def short_session_id(self) -> str: def short_session_id(self) -> str:
""" """
@ -1290,7 +1313,6 @@ class Session:
:return: nothing :return: nothing
""" """
logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) logging.info("booting node(%s): %s", node.name, [x.name for x in node.services])
self.add_remove_control_iface(node=node, remove=False)
self.services.boot_services(node) self.services.boot_services(node)
node.start_config_services() node.start_config_services()
@ -1305,11 +1327,10 @@ class Session:
with self.nodes_lock: with self.nodes_lock:
funcs = [] funcs = []
start = time.monotonic() start = time.monotonic()
for _id in self.nodes: for node in self.nodes.values():
node = self.nodes[_id] if isinstance(node, (CoreNode, PhysicalNode)):
if isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node): self.add_remove_control_iface(node, remove=False)
args = (node,) funcs.append((self.boot_node, (node,), {}))
funcs.append((self.boot_node, args, {}))
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
total = time.monotonic() - start total = time.monotonic() - start
logging.debug("boot run time: %s", total) logging.debug("boot run time: %s", total)
@ -1457,7 +1478,7 @@ class Session:
def add_remove_control_iface( def add_remove_control_iface(
self, self,
node: CoreNode, node: Union[CoreNode, PhysicalNode],
net_index: int = 0, net_index: int = 0,
remove: bool = False, remove: bool = False,
conf_required: bool = True, conf_required: bool = True,

View file

@ -582,6 +582,8 @@ class CoreClient:
# create edges config # create edges config
edges_config = [] edges_config = []
for edge in self.links.values(): for edge in self.links.values():
if not edge.is_customized():
continue
edge_config = dict(token=edge.token, width=edge.width, color=edge.color) edge_config = dict(token=edge.token, width=edge.width, color=edge.color)
edges_config.append(edge_config) edges_config.append(edge_config)
edges_config = json.dumps(edges_config) edges_config = json.dumps(edges_config)

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Optional
import netaddr import netaddr
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Node from core.api.grpc.wrappers import Interface, Node
from core.gui import nodeutils, validation from core.gui import nodeutils, validation
from core.gui.appconfig import ICONS_PATH from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -21,8 +21,11 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
IFACE_NAME_LEN: int = 15
DEFAULT_SERVER: str = "localhost"
def check_ip6(parent, name: str, value: str) -> bool:
def check_ip6(parent: tk.BaseWidget, name: str, value: str) -> bool:
if not value: if not value:
return True return True
title = f"IP6 Error for {name}" title = f"IP6 Error for {name}"
@ -47,7 +50,7 @@ def check_ip6(parent, name: str, value: str) -> bool:
return True return True
def check_ip4(parent, name: str, value: str) -> bool: def check_ip4(parent: tk.BaseWidget, name: str, value: str) -> bool:
if not value: if not value:
return True return True
title = f"IP4 Error for {name}" title = f"IP4 Error for {name}"
@ -84,16 +87,88 @@ def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry, mac: tk.StringVar) -> Non
class InterfaceData: class InterfaceData:
def __init__( def __init__(
self, self,
name: tk.StringVar,
is_auto: tk.BooleanVar, is_auto: tk.BooleanVar,
mac: tk.StringVar, mac: tk.StringVar,
ip4: tk.StringVar, ip4: tk.StringVar,
ip6: tk.StringVar, ip6: tk.StringVar,
) -> None: ) -> None:
self.name: tk.StringVar = name
self.is_auto: tk.BooleanVar = is_auto self.is_auto: tk.BooleanVar = is_auto
self.mac: tk.StringVar = mac self.mac: tk.StringVar = mac
self.ip4: tk.StringVar = ip4 self.ip4: tk.StringVar = ip4
self.ip6: tk.StringVar = ip6 self.ip6: tk.StringVar = ip6
def validate(self, parent: tk.BaseWidget, iface: Interface) -> bool:
valid_name = self._validate_name(parent, iface)
valid_ip4 = self._validate_ip4(parent, iface)
valid_ip6 = self._validate_ip6(parent, iface)
valid_mac = self._validate_mac(parent, iface)
return all([valid_name, valid_ip4, valid_ip6, valid_mac])
def _validate_name(self, parent: tk.BaseWidget, iface: Interface) -> bool:
name = self.name.get()
title = f"Interface Name Error for {iface.name}"
if not name:
messagebox.showerror(title, "Name cannot be empty", parent=parent)
return False
if len(name) > IFACE_NAME_LEN:
messagebox.showerror(
title,
f"Name cannot be greater than {IFACE_NAME_LEN} chars",
parent=parent,
)
return False
for x in name:
if x.isspace() or x == "/":
messagebox.showerror(
title, "Name cannot contain space or /", parent=parent
)
return False
iface.name = name
return True
def _validate_ip4(self, parent: tk.BaseWidget, iface: Interface) -> bool:
ip4_net = self.ip4.get()
if not check_ip4(parent, iface.name, ip4_net):
return False
if ip4_net:
ip4, ip4_mask = ip4_net.split("/")
ip4_mask = int(ip4_mask)
else:
ip4, ip4_mask = "", 0
iface.ip4 = ip4
iface.ip4_mask = ip4_mask
return True
def _validate_ip6(self, parent: tk.BaseWidget, iface: Interface) -> bool:
ip6_net = self.ip6.get()
if not check_ip6(parent, iface.name, ip6_net):
return False
if ip6_net:
ip6, ip6_mask = ip6_net.split("/")
ip6_mask = int(ip6_mask)
else:
ip6, ip6_mask = "", 0
iface.ip6 = ip6
iface.ip6_mask = ip6_mask
return True
def _validate_mac(self, parent: tk.BaseWidget, iface: Interface) -> bool:
mac = self.mac.get()
auto_mac = self.is_auto.get()
if auto_mac:
iface.mac = None
else:
if not netaddr.valid_mac(mac):
title = f"MAC Error for {iface.name}"
messagebox.showerror(title, "Invalid MAC Address", parent=parent)
return False
else:
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
iface.mac = str(mac)
return True
class NodeConfigDialog(Dialog): class NodeConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
@ -109,7 +184,7 @@ class NodeConfigDialog(Dialog):
self.name: tk.StringVar = tk.StringVar(value=self.node.name) self.name: tk.StringVar = tk.StringVar(value=self.node.name)
self.type: tk.StringVar = tk.StringVar(value=self.node.model) self.type: tk.StringVar = tk.StringVar(value=self.node.model)
self.container_image: tk.StringVar = tk.StringVar(value=self.node.image) self.container_image: tk.StringVar = tk.StringVar(value=self.node.image)
server = "localhost" server = DEFAULT_SERVER
if self.node.server: if self.node.server:
server = self.node.server server = self.node.server
self.server: tk.StringVar = tk.StringVar(value=server) self.server: tk.StringVar = tk.StringVar(value=server)
@ -176,7 +251,7 @@ class NodeConfigDialog(Dialog):
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Server") label = ttk.Label(frame, text="Server")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
servers = ["localhost"] servers = [DEFAULT_SERVER]
servers.extend(list(sorted(self.app.core.servers.keys()))) servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, textvariable=self.server, values=servers, state=combo_state frame, textvariable=self.server, values=servers, state=combo_state
@ -229,6 +304,14 @@ class NodeConfigDialog(Dialog):
button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY) button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY)
row += 1 row += 1
label = ttk.Label(tab, text="Name")
label.grid(row=row, column=0, padx=PADX, pady=PADY)
name = tk.StringVar(value=iface.name)
entry = ttk.Entry(tab, textvariable=name, state=state)
entry.var = name
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
row += 1
label = ttk.Label(tab, text="MAC") label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
auto_set = not iface.mac auto_set = not iface.mac
@ -267,7 +350,7 @@ class NodeConfigDialog(Dialog):
entry = ttk.Entry(tab, textvariable=ip6, state=state) entry = ttk.Entry(tab, textvariable=ip6, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW) entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) self.ifaces[iface.id] = InterfaceData(name, is_auto, mac, ip4, ip6)
def draw_buttons(self) -> None: def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
@ -300,7 +383,10 @@ class NodeConfigDialog(Dialog):
if NodeUtils.is_image_node(self.node.type): if NodeUtils.is_image_node(self.node.type):
self.node.image = self.container_image.get() self.node.image = self.container_image.get()
server = self.server.get() server = self.server.get()
if NodeUtils.is_container_node(self.node.type) and server != "localhost": if NodeUtils.is_container_node(self.node.type):
if server == DEFAULT_SERVER:
self.node.server = None
else:
self.node.server = server self.node.server = server
# set custom icon # set custom icon
@ -313,43 +399,9 @@ class NodeConfigDialog(Dialog):
# update node interface data # update node interface data
for iface in self.canvas_node.ifaces.values(): for iface in self.canvas_node.ifaces.values():
data = self.ifaces[iface.id] data = self.ifaces[iface.id]
error = not data.validate(self, iface)
# validate ip4 if error:
ip4_net = data.ip4.get()
if not check_ip4(self, iface.name, ip4_net):
error = True
break break
if ip4_net:
ip4, ip4_mask = ip4_net.split("/")
ip4_mask = int(ip4_mask)
else:
ip4, ip4_mask = "", 0
iface.ip4 = ip4
iface.ip4_mask = ip4_mask
# validate ip6
ip6_net = data.ip6.get()
if not check_ip6(self, iface.name, ip6_net):
error = True
break
if ip6_net:
ip6, ip6_mask = ip6_net.split("/")
ip6_mask = int(ip6_mask)
else:
ip6, ip6_mask = "", 0
iface.ip6 = ip6
iface.ip6_mask = ip6_mask
mac = data.mac.get()
auto_mac = data.is_auto.get()
if not auto_mac and not netaddr.valid_mac(mac):
title = f"MAC Error for {iface.name}"
messagebox.showerror(title, "Invalid MAC Address")
error = True
break
elif not auto_mac:
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
iface.mac = str(mac)
# redraw # redraw
if not error: if not error:

View file

@ -28,8 +28,8 @@ def create_wireless_token(src: int, dst: int, network: int) -> str:
def create_edge_token(link: Link) -> str: def create_edge_token(link: Link) -> str:
iface1_id = link.iface1.id if link.iface1 else None iface1_id = link.iface1.id if link.iface1 else 0
iface2_id = link.iface2.id if link.iface2 else None iface2_id = link.iface2.id if link.iface2 else 0
return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}" return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}"
@ -297,6 +297,9 @@ class CanvasEdge(Edge):
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context() self.create_context()
def is_customized(self) -> bool:
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
def create_context(self) -> None: def create_context(self) -> None:
themes.style_menu(self.context) themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure) self.context.add_command(label="Configure", command=self.click_configure)

View file

@ -107,7 +107,7 @@ class NodeNameEntry(ValidationEntry):
if len(s) == 0: if len(s) == 0:
return True return True
for x in s: for x in s:
if not x.isalnum() and x != "_": if not x.isalnum() and x != "-":
return False return False
return True return True

View file

@ -31,6 +31,9 @@ from core.nodes.network import WlanNode
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
LEARNING_DISABLED: int = 0
LEARNING_ENABLED: int = 30000
def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]: def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]:
try: try:
@ -172,26 +175,6 @@ class MobilityManager(ModelManager):
) )
self.session.broadcast_event(event_data) self.session.broadcast_event(event_data)
def update_nets(
self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]
) -> None:
"""
A mobility script has caused nodes in the 'moved' list to move.
Update every mobility network. This saves range calculations if the model
were to recalculate for each individual node movement.
:param moved: moved nodes
:param moved_ifaces: moved network interfaces
:return: nothing
"""
for node_id in self.nodes():
try:
node = get_mobility_node(self.session, node_id)
if node.model:
node.model.update(moved, moved_ifaces)
except CoreError:
logging.exception("error updating mobility node")
class WirelessModel(ConfigurableOptions): class WirelessModel(ConfigurableOptions):
""" """
@ -223,11 +206,10 @@ class WirelessModel(ConfigurableOptions):
""" """
return [] return []
def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: List[CoreInterface]) -> None:
""" """
Update this wireless model. Update this wireless model.
:param moved: moved nodes
:param moved_ifaces: moved network interfaces :param moved_ifaces: moved network interfaces
:return: nothing :return: nothing
""" """
@ -280,6 +262,12 @@ class BasicRangeModel(WirelessModel):
Configuration( Configuration(
_id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)"
), ),
Configuration(
_id="promiscuous",
_type=ConfigDataTypes.BOOL,
default="0",
label="promiscuous mode",
),
] ]
@classmethod @classmethod
@ -303,6 +291,7 @@ class BasicRangeModel(WirelessModel):
self.delay: Optional[int] = None self.delay: Optional[int] = None
self.loss: Optional[float] = None self.loss: Optional[float] = None
self.jitter: Optional[int] = None self.jitter: Optional[int] = None
self.promiscuous: bool = False
def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int: def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int:
""" """
@ -369,14 +358,13 @@ class BasicRangeModel(WirelessModel):
position_callback = set_position position_callback = set_position
def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: List[CoreInterface]) -> None:
""" """
Node positions have changed without recalc. Update positions from Node positions have changed without recalc. Update positions from
node.position, then re-calculate links for those that have moved. node.position, then re-calculate links for those that have moved.
Assumes bidirectional links, with one calculation per node pair, where Assumes bidirectional links, with one calculation per node pair, where
one of the nodes has moved. one of the nodes has moved.
:param moved: moved nodes
:param moved_ifaces: moved network interfaces :param moved_ifaces: moved network interfaces
:return: nothing :return: nothing
""" """
@ -420,7 +408,6 @@ class BasicRangeModel(WirelessModel):
with self.wlan._linked_lock: with self.wlan._linked_lock:
linked = self.wlan.linked(a, b) linked = self.wlan.linked(a, b)
if d > self.range: if d > self.range:
if linked: if linked:
logging.debug("was linked, unlinking") logging.debug("was linked, unlinking")
@ -467,6 +454,12 @@ class BasicRangeModel(WirelessModel):
self.delay = self._get_config(self.delay, config, "delay") self.delay = self._get_config(self.delay, config, "delay")
self.loss = self._get_config(self.loss, config, "error") self.loss = self._get_config(self.loss, config, "error")
self.jitter = self._get_config(self.jitter, config, "jitter") self.jitter = self._get_config(self.jitter, config, "jitter")
promiscuous = config["promiscuous"] == "1"
if self.promiscuous and not promiscuous:
self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED)
elif not self.promiscuous and promiscuous:
self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_DISABLED)
self.promiscuous = promiscuous
self.setlinkparams() self.setlinkparams()
def create_link_data( def create_link_data(
@ -638,17 +631,14 @@ class WayPointMobility(WirelessModel):
return return
return self.run() return self.run()
# only move interfaces attached to self.wlan, or all nodenum in script?
moved = []
moved_ifaces = [] moved_ifaces = []
for iface in self.net.get_ifaces(): for iface in self.net.get_ifaces():
node = iface.node node = iface.node
if self.movenode(node, dt): if self.movenode(node, dt):
moved.append(node)
moved_ifaces.append(iface) moved_ifaces.append(iface)
# calculate all ranges after moving nodes; this saves calculations # calculate all ranges after moving nodes; this saves calculations
self.session.mobility.update_nets(moved, moved_ifaces) self.net.model.update(moved_ifaces)
# TODO: check session state # TODO: check session state
self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
@ -659,7 +649,6 @@ class WayPointMobility(WirelessModel):
:return: nothing :return: nothing
""" """
logging.info("running mobility scenario")
self.timezero = time.monotonic() self.timezero = time.monotonic()
self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.lasttime = self.timezero - (0.001 * self.refresh_ms)
self.movenodesinitial() self.movenodesinitial()
@ -719,7 +708,6 @@ class WayPointMobility(WirelessModel):
:return: nothing :return: nothing
""" """
moved = []
moved_ifaces = [] moved_ifaces = []
for iface in self.net.get_ifaces(): for iface in self.net.get_ifaces():
node = iface.node node = iface.node
@ -727,9 +715,8 @@ class WayPointMobility(WirelessModel):
continue continue
x, y, z = self.initial[node.id].coords x, y, z = self.initial[node.id].coords
self.setnodeposition(node, x, y, z) self.setnodeposition(node, x, y, z)
moved.append(node)
moved_ifaces.append(iface) moved_ifaces.append(iface)
self.session.mobility.update_nets(moved, moved_ifaces) self.net.model.update(moved_ifaces)
def addwaypoint( def addwaypoint(
self, self,
@ -1114,7 +1101,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
logging.info("starting script") logging.info("starting script: %s", self.file)
laststate = self.state laststate = self.state
super().start() super().start()
if laststate == self.STATE_PAUSED: if laststate == self.STATE_PAUSED:
@ -1135,6 +1122,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
logging.info("pausing script: %s", self.file)
super().pause() super().pause()
self.statescript("pause") self.statescript("pause")
@ -1146,6 +1134,7 @@ class Ns2ScriptedMobility(WayPointMobility):
position position
:return: nothing :return: nothing
""" """
logging.info("stopping script: %s", self.file)
super().stop(move_initial=move_initial) super().stop(move_initial=move_initial)
self.statescript("stop") self.statescript("stop")

View file

@ -837,7 +837,12 @@ class CoreNode(CoreNodeBase):
if net.has_custom_iface: if net.has_custom_iface:
return net.custom_iface(self, iface_data) return net.custom_iface(self, iface_data)
else: else:
iface_id = self.newveth(iface_data.id, iface_data.name) iface_id = iface_data.id
if iface_id is not None and iface_id in self.ifaces:
raise CoreError(
f"node({self.name}) already has interface({iface_id})"
)
iface_id = self.newveth(iface_id, iface_data.name)
self.attachnet(iface_id, net) self.attachnet(iface_id, net)
if iface_data.mac: if iface_data.mac:
self.set_mac(iface_id, iface_data.mac) self.set_mac(iface_id, iface_data.mac)

View file

@ -241,12 +241,16 @@ class CoreInterface:
jitter = self.getparam("jitter") jitter = self.getparam("jitter")
if jitter is not None: if jitter is not None:
jitter = int(jitter) jitter = int(jitter)
buffer = self.getparam("buffer")
if buffer is not None:
buffer = int(buffer)
return LinkOptions( return LinkOptions(
delay=delay, delay=delay,
bandwidth=bandwidth, bandwidth=bandwidth,
dup=dup, dup=dup,
jitter=jitter, jitter=jitter,
loss=self.getparam("loss"), loss=self.getparam("loss"),
buffer=buffer,
unidirectional=unidirectional, unidirectional=unidirectional,
) )

View file

@ -286,14 +286,15 @@ class LinuxNetClient:
return True return True
return False return False
def disable_mac_learning(self, name: str) -> None: def set_mac_learning(self, name: str, value: int) -> None:
""" """
Disable mac learning for a Linux bridge. Set mac learning for a Linux bridge.
:param name: bridge name :param name: bridge name
:param value: ageing time value
:return: nothing :return: nothing
""" """
self.run(f"{IP} link set {name} type bridge ageing_time 0") self.run(f"{IP} link set {name} type bridge ageing_time {value}")
class OvsNetClient(LinuxNetClient): class OvsNetClient(LinuxNetClient):

View file

@ -32,7 +32,8 @@ if TYPE_CHECKING:
WirelessModelType = Type[WirelessModel] WirelessModelType = Type[WirelessModel]
ebtables_lock = threading.Lock() LEARNING_DISABLED: int = 0
ebtables_lock: threading.Lock = threading.Lock()
class EbtablesQueue: class EbtablesQueue:
@ -946,7 +947,7 @@ class HubNode(CoreNetwork):
:return: nothing :return: nothing
""" """
super().startup() super().startup()
self.net_client.disable_mac_learning(self.brname) self.net_client.set_mac_learning(self.brname, LEARNING_DISABLED)
class WlanNode(CoreNetwork): class WlanNode(CoreNetwork):
@ -989,7 +990,6 @@ class WlanNode(CoreNetwork):
:return: nothing :return: nothing
""" """
super().startup() super().startup()
self.net_client.disable_mac_learning(self.brname)
ebq.ebchange(self) ebq.ebchange(self)
def attach(self, iface: CoreInterface) -> None: def attach(self, iface: CoreInterface) -> None:

View file

@ -266,7 +266,9 @@ class Rj45Node(CoreNodeBase):
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.iface = CoreInterface(session, self, name, name, mtu, server) self.iface: CoreInterface = CoreInterface(
session, self, name, name, mtu, server
)
self.iface.transport_type = TransportType.RAW self.iface.transport_type = TransportType.RAW
self.lock: threading.RLock = threading.RLock() self.lock: threading.RLock = threading.RLock()
self.iface_id: Optional[int] = None self.iface_id: Optional[int] = None
@ -335,10 +337,11 @@ class Rj45Node(CoreNodeBase):
if iface_id is None: if iface_id is None:
iface_id = 0 iface_id = 0
if self.iface.net is not None: if self.iface.net is not None:
raise CoreError("RJ45 nodes support at most 1 network interface") raise CoreError(
f"RJ45({self.name}) nodes support at most 1 network interface"
)
self.ifaces[iface_id] = self.iface self.ifaces[iface_id] = self.iface
self.iface_id = iface_id self.iface_id = iface_id
if net is not None:
self.iface.attachnet(net) self.iface.attachnet(net)
for ip in iface_data.get_ips(): for ip in iface_data.get_ips():
self.add_ip(ip) self.add_ip(ip)
@ -353,6 +356,12 @@ class Rj45Node(CoreNodeBase):
""" """
self.get_iface(iface_id) self.get_iface(iface_id)
self.ifaces.pop(iface_id) self.ifaces.pop(iface_id)
if self.iface.net is None:
raise CoreError(
f"RJ45({self.name}) is not currently connected to a network"
)
self.iface.detachnet()
self.iface.net = None
self.shutdown() self.shutdown()
def get_iface(self, iface_id: int) -> CoreInterface: def get_iface(self, iface_id: int) -> CoreInterface:

View file

@ -4,7 +4,6 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging import logging
import socket import socket
import threading
from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
@ -65,7 +64,6 @@ class Sdt:
:param session: session this manager is tied to :param session: session this manager is tied to
""" """
self.session: "Session" = session self.session: "Session" = session
self.lock: threading.Lock = threading.Lock()
self.sock: Optional[IO] = None self.sock: Optional[IO] = None
self.connected: bool = False self.connected: bool = False
self.url: str = self.DEFAULT_SDT_URL self.url: str = self.DEFAULT_SDT_URL

View file

@ -570,6 +570,7 @@ class CoreXmlWriter:
add_attribute(options, "unidirectional", options_data.unidirectional) add_attribute(options, "unidirectional", options_data.unidirectional)
add_attribute(options, "network_id", link_data.network_id) add_attribute(options, "network_id", link_data.network_id)
add_attribute(options, "key", options_data.key) add_attribute(options, "key", options_data.key)
add_attribute(options, "buffer", options_data.buffer)
if options.items(): if options.items():
link_element.append(options) link_element.append(options)
@ -976,6 +977,7 @@ class CoreXmlReader:
if options.loss is None: if options.loss is None:
options.loss = get_float(options_element, "per") options.loss = get_float(options_element, "per")
options.unidirectional = get_int(options_element, "unidirectional") options.unidirectional = get_int(options_element, "unidirectional")
options.buffer = get_int(options_element, "buffer")
if options.unidirectional == 1 and node_set in node_sets: if options.unidirectional == 1 and node_set in node_sets:
logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id)

View file

@ -74,10 +74,14 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
"-a", "-a",
"--address", "--address",
required=True,
help="local address that distributed servers will use for gre tunneling", help="local address that distributed servers will use for gre tunneling",
) )
parser.add_argument( parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes" "-s",
"--server",
required=True,
help="distributed server to use for creating nodes",
) )
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "core" name = "core"
version = "7.3.0" version = "7.4.0"
description = "CORE Common Open Research Emulator" description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"] authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause" license = "BSD-2-Clause"

View file

@ -309,6 +309,7 @@ class TestXml:
options.jitter = 10 options.jitter = 10
options.delay = 30 options.delay = 30
options.dup = 5 options.dup = 5
options.buffer = 100
session.add_link(node1.id, switch.id, iface1_data, options=options) session.add_link(node1.id, switch.id, iface1_data, options=options)
# instantiate session # instantiate session
@ -352,6 +353,7 @@ class TestXml:
assert options.jitter == link.options.jitter assert options.jitter == link.options.jitter
assert options.delay == link.options.delay assert options.delay == link.options.delay
assert options.dup == link.options.dup assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_ptp( def test_link_options_ptp(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -376,6 +378,7 @@ class TestXml:
options.jitter = 10 options.jitter = 10
options.delay = 30 options.delay = 30
options.dup = 5 options.dup = 5
options.buffer = 100
session.add_link(node1.id, node2.id, iface1_data, iface2_data, options) session.add_link(node1.id, node2.id, iface1_data, iface2_data, options)
# instantiate session # instantiate session
@ -419,6 +422,7 @@ class TestXml:
assert options.jitter == link.options.jitter assert options.jitter == link.options.jitter
assert options.delay == link.options.delay assert options.delay == link.options.delay
assert options.dup == link.options.dup assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_bidirectional( def test_link_options_bidirectional(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -444,6 +448,7 @@ class TestXml:
options1.loss = 10.5 options1.loss = 10.5
options1.dup = 5 options1.dup = 5
options1.jitter = 5 options1.jitter = 5
options1.buffer = 50
session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1)
options2 = LinkOptions() options2 = LinkOptions()
options2.unidirectional = 1 options2.unidirectional = 1
@ -452,6 +457,7 @@ class TestXml:
options2.loss = 10 options2.loss = 10
options2.dup = 10 options2.dup = 10
options2.jitter = 10 options2.jitter = 10
options2.buffer = 100
session.update_link( session.update_link(
node2.id, node1.id, iface2_data.id, iface1_data.id, options2 node2.id, node1.id, iface2_data.id, iface1_data.id, options2
) )
@ -499,8 +505,10 @@ class TestXml:
assert options1.loss == link1.options.loss assert options1.loss == link1.options.loss
assert options1.dup == link1.options.dup assert options1.dup == link1.options.dup
assert options1.jitter == link1.options.jitter assert options1.jitter == link1.options.jitter
assert options1.buffer == link1.options.buffer
assert options2.bandwidth == link2.options.bandwidth assert options2.bandwidth == link2.options.bandwidth
assert options2.delay == link2.options.delay assert options2.delay == link2.options.delay
assert options2.loss == link2.options.loss assert options2.loss == link2.options.loss
assert options2.dup == link2.options.dup assert options2.dup == link2.options.dup
assert options2.jitter == link2.options.jitter assert options2.jitter == link2.options.jitter
assert options2.buffer == link2.options.buffer

View file

@ -100,6 +100,7 @@ class OsInfo:
if not os_like: if not os_like:
like = " ".join(like) like = " ".join(like)
print(f"unsupported os install type({like})") print(f"unsupported os install type({like})")
print("trying using the -i option to specify an install type")
sys.exit(1) sys.exit(1)
if version: if version:
try: try:
@ -141,7 +142,7 @@ def get_os(install_type: Optional[str]) -> OsInfo:
key, value = line.split("=") key, value = line.split("=")
d[key] = value.strip("\"") d[key] = value.strip("\"")
name_value = d["ID"] name_value = d["ID"]
like_value = d["ID_LIKE"] like_value = d.get("ID_LIKE", "")
version_value = d["VERSION_ID"] version_value = d["VERSION_ID"]
return OsInfo.get(name_value, like_value.split(), version_value) return OsInfo.get(name_value, like_value.split(), version_value)