daemon: adjustments to revamp how core nodes are created in session.add_node, nodes now provide a create_options function for node specific options that are type hinted

This commit is contained in:
Blake Harnden 2022-05-25 10:51:42 -07:00
parent 03e646031c
commit 2e3e085522
35 changed files with 646 additions and 478 deletions

View file

@ -5,6 +5,7 @@ import abc
import logging
import shutil
import threading
from dataclasses import dataclass, field
from pathlib import Path
from threading import RLock
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
@ -33,6 +34,94 @@ if TYPE_CHECKING:
PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")]
@dataclass
class Position:
"""
Helper class for Cartesian coordinate position
"""
x: float = 0.0
y: float = 0.0
z: float = 0.0
lon: float = None
lat: float = None
alt: float = None
def set(self, x: float = None, y: float = None, z: float = None) -> bool:
"""
Returns True if the position has actually changed.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
"""
if self.x == x and self.y == y and self.z == z:
return False
self.x = x
self.y = y
self.z = z
return True
def get(self) -> Tuple[float, float, float]:
"""
Retrieve x,y,z position.
:return: x,y,z position tuple
"""
return self.x, self.y, self.z
def has_geo(self) -> bool:
return all(x is not None for x in [self.lon, self.lat, self.alt])
def set_geo(self, lon: float, lat: float, alt: float) -> None:
"""
Set geo position lon, lat, alt.
:param lon: longitude value
:param lat: latitude value
:param alt: altitude value
:return: nothing
"""
self.lon = lon
self.lat = lat
self.alt = alt
def get_geo(self) -> Tuple[float, float, float]:
"""
Retrieve current geo position lon, lat, alt.
:return: lon, lat, alt position tuple
"""
return self.lon, self.lat, self.alt
@dataclass
class NodeOptions:
"""
Base options for configuring a node.
"""
canvas: int = None
"""id of canvas for display within gui"""
icon: str = None
"""custom icon for display, None for default"""
@dataclass
class CoreNodeOptions(NodeOptions):
model: str = "PC"
"""model is used for providing a default set of services"""
services: List[str] = field(default_factory=list)
"""services to start within node"""
config_services: List[str] = field(default_factory=list)
"""config services to start within node"""
directory: Path = None
"""directory to define node, defaults to path under the session directory"""
legacy: bool = False
"""legacy nodes default to standard services"""
class NodeBase(abc.ABC):
"""
Base class for CORE nodes (nodes and networks)
@ -44,6 +133,7 @@ class NodeBase(abc.ABC):
_id: int = None,
name: str = None,
server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None:
"""
Creates a NodeBase instance.
@ -53,26 +143,29 @@ class NodeBase(abc.ABC):
:param name: object name
:param server: remote server node
will run on, default is None for localhost
:param options: options to create node with
"""
self.session: "Session" = session
if _id is None:
_id = session.next_node_id()
self.id: int = _id
self.name: str = name or f"o{self.id}"
self.id: int = _id if _id is not None else self.session.next_node_id()
self.name: str = name or f"{self.__class__.__name__}{self.id}"
self.server: "DistributedServer" = server
self.model: Optional[str] = None
self.services: CoreServices = []
self.ifaces: Dict[int, CoreInterface] = {}
self.iface_id: int = 0
self.canvas: Optional[int] = None
self.icon: Optional[str] = None
self.position: Position = Position()
self.up: bool = False
self.lock: RLock = RLock()
self.net_client: LinuxNetClient = get_net_client(
self.session.use_ovs(), self.host_cmd
)
options = options if options else NodeOptions()
self.canvas: Optional[int] = options.canvas
self.icon: Optional[str] = options.icon
@classmethod
def create_options(cls) -> NodeOptions:
return NodeOptions()
@abc.abstractmethod
def startup(self) -> None:
@ -288,6 +381,7 @@ class CoreNodeBase(NodeBase):
_id: int = None,
name: str = None,
server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None:
"""
Create a CoreNodeBase instance.
@ -298,7 +392,7 @@ class CoreNodeBase(NodeBase):
:param server: remote server node
will run on, default is None for localhost
"""
super().__init__(session, _id, name, server)
super().__init__(session, _id, name, server, options)
self.config_services: Dict[str, "ConfigService"] = {}
self.directory: Optional[Path] = None
self.tmpnodedir: bool = False
@ -460,8 +554,8 @@ class CoreNode(CoreNodeBase):
session: "Session",
_id: int = None,
name: str = None,
directory: Path = None,
server: "DistributedServer" = None,
options: CoreNodeOptions = None,
) -> None:
"""
Create a CoreNode instance.
@ -469,18 +563,37 @@ class CoreNode(CoreNodeBase):
:param session: core session instance
:param _id: object id
:param name: object name
:param directory: node directory
:param server: remote server node
will run on, default is None for localhost
:param options: options to create node with
"""
super().__init__(session, _id, name, server)
self.directory: Optional[Path] = directory
options = options or CoreNodeOptions()
super().__init__(session, _id, name, server, options)
self.directory: Optional[Path] = options.directory
self.ctrlchnlname: Path = self.session.directory / self.name
self.pid: Optional[int] = None
self._mounts: List[Tuple[Path, Path]] = []
self.node_net_client: LinuxNetClient = self.create_node_net_client(
self.session.use_ovs()
)
options = options or CoreNodeOptions()
self.model: Optional[str] = options.model
# setup services
if options.legacy or options.services:
logger.debug("set node type: %s", self.model)
self.session.services.add_services(self, self.model, options.services)
# add config services
config_services = options.config_services
if not options.legacy and not config_services and not options.services:
config_services = self.session.services.default_services.get(self.model, [])
logger.info("setting node config services: %s", config_services)
for name in config_services:
service_class = self.session.service_manager.get_service(name)
self.add_config_service(service_class)
@classmethod
def create_options(cls) -> CoreNodeOptions:
return CoreNodeOptions()
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient:
"""
@ -797,6 +910,7 @@ class CoreNetworkBase(NodeBase):
_id: int,
name: str,
server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None:
"""
Create a CoreNetworkBase instance.
@ -806,9 +920,11 @@ class CoreNetworkBase(NodeBase):
:param name: object name
:param server: remote server node
will run on, default is None for localhost
:param options: options to create node with
"""
super().__init__(session, _id, name, server)
self.mtu: int = DEFAULT_MTU
super().__init__(session, _id, name, server, options)
mtu = self.session.options.get_int("mtu")
self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU
self.brname: Optional[str] = None
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
self.linked_lock: threading.Lock = threading.Lock()
@ -839,69 +955,3 @@ class CoreNetworkBase(NodeBase):
iface.net_id = None
with self.linked_lock:
del self.linked[iface]
class Position:
"""
Helper class for Cartesian coordinate position
"""
def __init__(self, x: float = None, y: float = None, z: float = None) -> None:
"""
Creates a Position instance.
:param x: x position
:param y: y position
:param z: z position
"""
self.x: float = x
self.y: float = y
self.z: float = z
self.lon: Optional[float] = None
self.lat: Optional[float] = None
self.alt: Optional[float] = None
def set(self, x: float = None, y: float = None, z: float = None) -> bool:
"""
Returns True if the position has actually changed.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
"""
if self.x == x and self.y == y and self.z == z:
return False
self.x = x
self.y = y
self.z = z
return True
def get(self) -> Tuple[float, float, float]:
"""
Retrieve x,y,z position.
:return: x,y,z position tuple
"""
return self.x, self.y, self.z
def set_geo(self, lon: float, lat: float, alt: float) -> None:
"""
Set geo position lon, lat, alt.
:param lon: longitude value
:param lat: latitude value
:param alt: altitude value
:return: nothing
"""
self.lon = lon
self.lat = lat
self.alt = alt
def get_geo(self) -> Tuple[float, float, float]:
"""
Retrieve current geo position lon, lat, alt.
:return: lon, lat, alt position tuple
"""
return self.lon, self.lat, self.alt

View file

@ -1,7 +1,7 @@
import json
import logging
import shlex
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Dict, List, Tuple
@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Dict, List, Tuple
from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError, CoreError
from core.executables import BASH
from core.nodes.base import CoreNode
from core.nodes.base import CoreNode, CoreNodeOptions
logger = logging.getLogger(__name__)
@ -19,6 +19,21 @@ if TYPE_CHECKING:
DOCKER: str = "docker"
@dataclass
class DockerOptions(CoreNodeOptions):
image: str = "ubuntu"
"""image used when creating container"""
binds: List[Tuple[str, str]] = field(default_factory=list)
"""bind mount source and destinations to setup within container"""
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list)
"""
volume mount source, destination, unique, delete to setup within container
unique is True for node unique volume naming
delete is True for deleting volume mount during shutdown
"""
@dataclass
class DockerVolume:
src: str
@ -38,11 +53,8 @@ class DockerNode(CoreNode):
session: "Session",
_id: int = None,
name: str = None,
directory: str = None,
server: DistributedServer = None,
image: str = None,
binds: List[Tuple[str, str]] = None,
volumes: List[Tuple[str, str, bool, bool]] = None,
options: DockerOptions = None,
) -> None:
"""
Create a DockerNode instance.
@ -50,22 +62,23 @@ class DockerNode(CoreNode):
:param session: core session instance
:param _id: object id
:param name: object name
:param directory: node directory
:param server: remote server node
will run on, default is None for localhost
:param image: image to start container with
:param binds: bind mounts to set for the created container
:param volumes: volume mount settings to set for the created container
:param options: options for creating node
"""
super().__init__(session, _id, name, directory, server)
self.image: str = image if image is not None else "ubuntu"
self.binds: List[Tuple[str, str]] = binds or []
options = options or DockerOptions()
super().__init__(session, _id, name, server, options)
self.image: str = options.image
self.binds: List[Tuple[str, str]] = options.binds
self.volumes: Dict[str, DockerVolume] = {}
volumes = volumes or []
for src, dst, unique, delete in volumes:
for src, dst, unique, delete in options.volumes:
src_name = self._unique_name(src) if unique else src
self.volumes[src] = DockerVolume(src_name, dst, unique, delete)
@classmethod
def create_options(cls) -> DockerOptions:
return DockerOptions()
def _create_cmd(self, args: str, shell: bool = False) -> str:
"""
Create command used to run commands within the context of a node.

View file

@ -1,15 +1,16 @@
import json
import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from core import utils
from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError
from core.nodes.base import CoreNode
from core.nodes.base import CoreNode, CoreNodeOptions
from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__)
@ -66,15 +67,29 @@ class LxdClient:
self.run(args)
@dataclass
class LxcOptions(CoreNodeOptions):
image: str = "ubuntu"
"""image used when creating container"""
binds: List[Tuple[str, str]] = field(default_factory=list)
"""bind mount source and destinations to setup within container"""
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list)
"""
volume mount source, destination, unique, delete to setup within container
unique is True for node unique volume naming
delete is True for deleting volume mount during shutdown
"""
class LxcNode(CoreNode):
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
directory: str = None,
server: DistributedServer = None,
image: str = None,
options: LxcOptions = None,
) -> None:
"""
Create a LxcNode instance.
@ -82,15 +97,19 @@ class LxcNode(CoreNode):
:param session: core session instance
:param _id: object id
:param name: object name
:param directory: node directory
:param server: remote server node
will run on, default is None for localhost
:param image: image to start container with
:param options: option to create node with
"""
super().__init__(session, _id, name, directory, server)
self.image: str = image if image is not None else "ubuntu"
options = options or LxcOptions()
super().__init__(session, _id, name, server, options)
self.image: str = options.image
self.client: Optional[LxdClient] = None
@classmethod
def create_options(cls) -> LxcOptions:
return LxcOptions()
def alive(self) -> bool:
"""
Check if the node is alive.

View file

@ -4,6 +4,7 @@ Defines network nodes used within core.
import logging
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Type
@ -14,7 +15,7 @@ from core.emulator.data import InterfaceData, LinkData
from core.emulator.enumerations import MessageFlags, NetworkPolicy, RegisterTlvs
from core.errors import CoreCommandError, CoreError
from core.executables import NFTABLES
from core.nodes.base import CoreNetworkBase
from core.nodes.base import CoreNetworkBase, NodeOptions
from core.nodes.interface import CoreInterface, GreTap
from core.nodes.netclient import get_net_client
@ -180,6 +181,12 @@ class NftablesQueue:
nft_queue: NftablesQueue = NftablesQueue()
@dataclass
class NetworkOptions(NodeOptions):
policy: NetworkPolicy = None
"""allows overriding the network policy, otherwise uses class defined default"""
class CoreNetwork(CoreNetworkBase):
"""
Provides linux bridge network functionality for core nodes.
@ -193,7 +200,7 @@ class CoreNetwork(CoreNetworkBase):
_id: int = None,
name: str = None,
server: "DistributedServer" = None,
policy: NetworkPolicy = None,
options: NetworkOptions = None,
) -> None:
"""
Creates a CoreNetwork instance.
@ -203,18 +210,19 @@ class CoreNetwork(CoreNetworkBase):
:param name: object name
:param server: remote server node
will run on, default is None for localhost
:param policy: network policy
:param options: options to create node with
"""
super().__init__(session, _id, name, server)
if name is None:
name = str(self.id)
if policy is not None:
self.policy: NetworkPolicy = policy
self.name: Optional[str] = name
options = options or NetworkOptions()
super().__init__(session, _id, name, server, options)
self.policy: NetworkPolicy = options.policy if options.policy else self.policy
sessionid = self.session.short_session_id()
self.brname: str = f"b.{self.id}.{sessionid}"
self.has_nftables_chain: bool = False
@classmethod
def create_options(cls) -> NetworkOptions:
return NetworkOptions()
def host_cmd(
self,
args: str,
@ -482,6 +490,20 @@ class GreTapBridge(CoreNetwork):
self.add_ips(ips)
@dataclass
class CtrlNetOptions(NetworkOptions):
prefix: str = None
"""ip4 network prefix to use for generating an address"""
updown_script: str = None
"""script to execute during startup and shutdown"""
serverintf: str = None
"""used to associate an interface with the control network bridge"""
assign_address: bool = True
"""used to determine if a specific address should be assign using hostid"""
hostid: int = None
"""used with assign address to """
class CtrlNet(CoreNetwork):
"""
Control network functionality.
@ -500,36 +522,32 @@ class CtrlNet(CoreNetwork):
def __init__(
self,
session: "Session",
prefix: str,
_id: int = None,
name: str = None,
hostid: int = None,
server: "DistributedServer" = None,
assign_address: bool = True,
updown_script: str = None,
serverintf: str = None,
options: CtrlNetOptions = None,
) -> None:
"""
Creates a CtrlNet instance.
:param session: core session instance
:param _id: node id
:param name: node namee
:param prefix: control network ipv4 prefix
:param hostid: host id
:param name: node name
:param server: remote server node
will run on, default is None for localhost
:param assign_address: assigned address
:param updown_script: updown script
:param serverintf: server interface
:return:
:param options: node options for creation
"""
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(prefix).cidr
self.hostid: Optional[int] = hostid
self.assign_address: bool = assign_address
self.updown_script: Optional[str] = updown_script
self.serverintf: Optional[str] = serverintf
super().__init__(session, _id, name, server)
options = options or CtrlNetOptions()
super().__init__(session, _id, name, server, options)
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(options.prefix).cidr
self.hostid: Optional[int] = options.hostid
self.assign_address: bool = options.assign_address
self.updown_script: Optional[str] = options.updown_script
self.serverintf: Optional[str] = options.serverintf
@classmethod
def create_options(cls) -> CtrlNetOptions:
return CtrlNetOptions()
def add_addresses(self, index: int) -> None:
"""
@ -669,7 +687,7 @@ class WlanNode(CoreNetwork):
_id: int = None,
name: str = None,
server: "DistributedServer" = None,
policy: NetworkPolicy = None,
options: NetworkOptions = None,
) -> None:
"""
Create a WlanNode instance.
@ -679,9 +697,9 @@ class WlanNode(CoreNetwork):
:param name: node name
:param server: remote server node
will run on, default is None for localhost
:param policy: wlan policy
:param options: options to create node with
"""
super().__init__(session, _id, name, server, policy)
super().__init__(session, _id, name, server, options)
# wireless and mobility models (BasicRangeModel, Ns2WaypointMobility)
self.wireless_model: Optional[WirelessModel] = None
self.mobility: Optional[WayPointMobility] = None

View file

@ -13,7 +13,7 @@ from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import TransportType
from core.errors import CoreCommandError, CoreError
from core.executables import BASH, TEST, UMOUNT
from core.nodes.base import CoreNode, CoreNodeBase
from core.nodes.base import CoreNode, CoreNodeBase, CoreNodeOptions, NodeOptions
from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__)
@ -34,6 +34,7 @@ class Rj45Node(CoreNodeBase):
_id: int = None,
name: str = None,
server: DistributedServer = None,
options: NodeOptions = None,
) -> None:
"""
Create an RJ45Node instance.
@ -43,8 +44,9 @@ class Rj45Node(CoreNodeBase):
:param name: node name
:param server: remote server node
will run on, default is None for localhost
:param options: option to create node with
"""
super().__init__(session, _id, name, server)
super().__init__(session, _id, name, server, options)
self.iface: CoreInterface = CoreInterface(
self.iface_id, name, name, session.use_ovs(), node=self, server=server
)
@ -224,12 +226,12 @@ class PhysicalNode(CoreNode):
session: "Session",
_id: int = None,
name: str = None,
directory: Path = None,
server: DistributedServer = None,
options: CoreNodeOptions = None,
) -> None:
if not self.server:
raise CoreError("physical nodes must be assigned to a remote server")
super().__init__(session, _id, name, directory, server)
super().__init__(session, _id, name, server, options)
def startup(self) -> None:
with self.lock:

View file

@ -14,7 +14,7 @@ from core.emulator.data import LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags
from core.errors import CoreError
from core.executables import NFTABLES
from core.nodes.base import CoreNetworkBase
from core.nodes.base import CoreNetworkBase, NodeOptions
from core.nodes.interface import CoreInterface
if TYPE_CHECKING:
@ -108,8 +108,9 @@ class WirelessNode(CoreNetworkBase):
_id: int,
name: str,
server: "DistributedServer" = None,
options: NodeOptions = None,
):
super().__init__(session, _id, name, server)
super().__init__(session, _id, name, server, options)
self.bridges: Dict[int, Tuple[CoreInterface, str]] = {}
self.links: Dict[Tuple[int, int], WirelessLink] = {}
self.position_enabled: bool = CONFIG_ENABLED