daemon: added initial podman node support

This commit is contained in:
Blake Harnden 2023-06-13 17:00:53 -07:00
parent c76bc2ee8a
commit a80796ac72
15 changed files with 310 additions and 35 deletions

View file

@ -964,9 +964,7 @@ class CoreGrpcClient:
:return: config service defaults :return: config service defaults
""" """
request = GetConfigServiceDefaultsRequest( request = GetConfigServiceDefaultsRequest(
name=name, name=name, session_id=session_id, node_id=node_id
session_id=session_id,
node_id=node_id,
) )
response = self.stub.GetConfigServiceDefaults(request) response = self.stub.GetConfigServiceDefaults(request)
return wrappers.ConfigServiceDefaults.from_proto(response) return wrappers.ConfigServiceDefaults.from_proto(response)

View file

@ -36,6 +36,7 @@ from core.nodes.docker import DockerNode, DockerOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode, LxcOptions from core.nodes.lxd import LxcNode, LxcOptions
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
from core.nodes.podman import PodmanNode, PodmanOptions
from core.nodes.wireless import WirelessNode from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -81,7 +82,7 @@ def add_node_data(
options.config_services = node_proto.config_services options.config_services = node_proto.config_services
if isinstance(options, EmaneOptions): if isinstance(options, EmaneOptions):
options.emane_model = node_proto.emane options.emane_model = node_proto.emane
if isinstance(options, (DockerOptions, LxcOptions)): if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
options.image = node_proto.image options.image = node_proto.image
position = Position() position = Position()
position.set(node_proto.position.x, node_proto.position.y) position.set(node_proto.position.x, node_proto.position.y)
@ -313,7 +314,7 @@ def get_node_proto(
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.wireless_model.name emane_model = node.wireless_model.name
image = None image = None
if isinstance(node, (DockerNode, LxcNode)): if isinstance(node, (DockerNode, LxcNode, PodmanNode)):
image = node.image image = node.image
# check for wlan config # check for wlan config
wlan_config = session.mobility.get_configs( wlan_config = session.mobility.get_configs(

View file

@ -68,6 +68,7 @@ class NodeType(Enum):
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17 WIRELESS = 17
PODMAN = 18
class LinkType(Enum): class LinkType(Enum):

View file

@ -12,15 +12,7 @@ from core.emulator.data import (
from core.errors import CoreError from core.errors import CoreError
T = TypeVar( T = TypeVar(
"T", "T", bound=Union[EventData, ExceptionData, NodeData, LinkData, FileData, ConfigData]
bound=Union[
EventData,
ExceptionData,
NodeData,
LinkData,
FileData,
ConfigData,
],
) )

View file

@ -50,6 +50,7 @@ class NodeTypes(Enum):
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17 WIRELESS = 17
PODMAN = 18
class LinkTypes(Enum): class LinkTypes(Enum):

View file

@ -43,7 +43,7 @@ class HookManager:
state_hooks = self.script_hooks.setdefault(state, {}) state_hooks = self.script_hooks.setdefault(state, {})
if file_name in state_hooks: if file_name in state_hooks:
raise CoreError( raise CoreError(
f"adding duplicate state({state.name}) hook script({file_name})", f"adding duplicate state({state.name}) hook script({file_name})"
) )
state_hooks[file_name] = data state_hooks[file_name] = data
@ -59,7 +59,7 @@ class HookManager:
if file_name not in state_hooks: if file_name not in state_hooks:
raise CoreError( raise CoreError(
f"deleting state({state.name}) hook script({file_name}) " f"deleting state({state.name}) hook script({file_name}) "
"that does not exist", "that does not exist"
) )
del state_hooks[file_name] del state_hooks[file_name]
@ -77,7 +77,7 @@ class HookManager:
if hook in hooks: if hook in hooks:
name = getattr(callable, "__name__", repr(hook)) name = getattr(callable, "__name__", repr(hook))
raise CoreError( raise CoreError(
f"adding duplicate state({state.name}) hook callback({name})", f"adding duplicate state({state.name}) hook callback({name})"
) )
hooks.append(hook) hooks.append(hook)
@ -96,7 +96,7 @@ class HookManager:
name = getattr(callable, "__name__", repr(hook)) name = getattr(callable, "__name__", repr(hook))
raise CoreError( raise CoreError(
f"deleting state({state.name}) hook callback({name}) " f"deleting state({state.name}) hook callback({name}) "
"that does not exist", "that does not exist"
) )
hooks.remove(hook) hooks.remove(hook)
@ -132,7 +132,7 @@ class HookManager:
except (OSError, subprocess.CalledProcessError) as e: except (OSError, subprocess.CalledProcessError) as e:
raise CoreError( raise CoreError(
f"failure running state({state.name}) " f"failure running state({state.name}) "
f"hook script({file_name}): {e}", f"hook script({file_name}): {e}"
) )
for hook in self.callback_hooks.get(state, []): for hook in self.callback_hooks.get(state, []):
try: try:
@ -141,5 +141,5 @@ class HookManager:
name = getattr(callable, "__name__", repr(hook)) name = getattr(callable, "__name__", repr(hook))
raise CoreError( raise CoreError(
f"failure running state({state.name}) " f"failure running state({state.name}) "
f"hook callback({name}): {e}", f"hook callback({name}): {e}"
) )

View file

@ -57,6 +57,7 @@ from core.nodes.network import (
WlanNode, WlanNode,
) )
from core.nodes.physical import PhysicalNode, Rj45Node from core.nodes.physical import PhysicalNode, Rj45Node
from core.nodes.podman import PodmanNode
from core.nodes.wireless import WirelessNode from core.nodes.wireless import WirelessNode
from core.plugins.sdt import Sdt from core.plugins.sdt import Sdt
from core.services.coreservices import CoreServices from core.services.coreservices import CoreServices
@ -81,9 +82,9 @@ NODES: dict[NodeTypes, type[NodeBase]] = {
NodeTypes.DOCKER: DockerNode, NodeTypes.DOCKER: DockerNode,
NodeTypes.LXC: LxcNode, NodeTypes.LXC: LxcNode,
NodeTypes.WIRELESS: WirelessNode, NodeTypes.WIRELESS: WirelessNode,
NodeTypes.PODMAN: PodmanNode,
} }
NODES_TYPE: dict[type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} NODES_TYPE: dict[type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES}
CONTAINER_NODES: set[type[NodeBase]] = {DockerNode, LxcNode}
CTRL_NET_ID: int = 9001 CTRL_NET_ID: int = 9001
LINK_COLORS: list[str] = ["green", "blue", "orange", "purple", "turquoise"] LINK_COLORS: list[str] = ["green", "blue", "orange", "purple", "turquoise"]
NT: TypeVar = TypeVar("NT", bound=NodeBase) NT: TypeVar = TypeVar("NT", bound=NodeBase)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -96,9 +96,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.red_label = ttk.Label( self.red_label = ttk.Label(
frame, frame, background=get_rgb(self.red.get(), 0, 0), width=5
background=get_rgb(self.red.get(), 0, 0),
width=5,
) )
self.red_label.grid(row=0, column=3, sticky=tk.EW) self.red_label.grid(row=0, column=3, sticky=tk.EW)
@ -121,9 +119,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.green_label = ttk.Label( self.green_label = ttk.Label(
frame, frame, background=get_rgb(0, self.green.get(), 0), width=5
background=get_rgb(0, self.green.get(), 0),
width=5,
) )
self.green_label.grid(row=0, column=3, sticky=tk.EW) self.green_label.grid(row=0, column=3, sticky=tk.EW)
@ -146,9 +142,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.blue_label = ttk.Label( self.blue_label = ttk.Label(
frame, frame, background=get_rgb(0, 0, self.blue.get()), width=5
background=get_rgb(0, 0, self.blue.get()),
width=5,
) )
self.blue_label.grid(row=0, column=3, sticky=tk.EW) self.blue_label.grid(row=0, column=3, sticky=tk.EW)

View file

@ -56,7 +56,7 @@ def node_label_positions(
src_x: int, src_y: int, dst_x: int, dst_y: int src_x: int, src_y: int, dst_x: int, dst_y: int
) -> tuple[tuple[float, float], tuple[float, float]]: ) -> tuple[tuple[float, float], tuple[float, float]]:
v_x, v_y = dst_x - src_x, dst_y - src_y v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x**2 + v_y**2) v_len = math.sqrt(v_x ** 2 + v_y ** 2)
if v_len == 0: if v_len == 0:
u_x, u_y = 0.0, 0.0 u_x, u_y = 0.0, 0.0
else: else:
@ -147,7 +147,7 @@ class Edge:
perp_m = -1 / m perp_m = -1 / m
b = mp_y - (perp_m * mp_x) b = mp_y - (perp_m * mp_x)
# get arc x and y # get arc x and y
offset = math.sqrt(self.arc**2 / (1 + (1 / m**2))) offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2)))
arc_x = mp_x arc_x = mp_x
if self.arc >= 0: if self.arc >= 0:
arc_x += offset arc_x += offset

View file

@ -78,6 +78,7 @@ class ImageEnum(Enum):
EDITDELETE = "edit-delete" EDITDELETE = "edit-delete"
ANTENNA = "antenna" ANTENNA = "antenna"
DOCKER = "docker" DOCKER = "docker"
PODMAN = "podman"
LXC = "lxc" LXC = "lxc"
ALERT = "alert" ALERT = "alert"
DELETE = "delete" DELETE = "delete"
@ -101,6 +102,7 @@ TYPE_MAP: dict[tuple[NodeType, str], ImageEnum] = {
(NodeType.RJ45, None): ImageEnum.RJ45, (NodeType.RJ45, None): ImageEnum.RJ45,
(NodeType.TUNNEL, None): ImageEnum.TUNNEL, (NodeType.TUNNEL, None): ImageEnum.TUNNEL,
(NodeType.DOCKER, None): ImageEnum.DOCKER, (NodeType.DOCKER, None): ImageEnum.DOCKER,
(NodeType.PODMAN, None): ImageEnum.PODMAN,
(NodeType.LXC, None): ImageEnum.LXC, (NodeType.LXC, None): ImageEnum.LXC,
} }

View file

@ -16,8 +16,13 @@ if TYPE_CHECKING:
NODES: list["NodeDraw"] = [] NODES: list["NodeDraw"] = []
NETWORK_NODES: list["NodeDraw"] = [] NETWORK_NODES: list["NodeDraw"] = []
NODE_ICONS = {} NODE_ICONS = {}
CONTAINER_NODES: set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} CONTAINER_NODES: set[NodeType] = {
IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC} NodeType.DEFAULT,
NodeType.DOCKER,
NodeType.LXC,
NodeType.PODMAN,
}
IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC, NodeType.PODMAN}
WIRELESS_NODES: set[NodeType] = { WIRELESS_NODES: set[NodeType] = {
NodeType.WIRELESS_LAN, NodeType.WIRELESS_LAN,
NodeType.EMANE, NodeType.EMANE,
@ -41,6 +46,7 @@ def setup() -> None:
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
(ImageEnum.LXC, NodeType.LXC, "LXC", None), (ImageEnum.LXC, NodeType.LXC, "LXC", None),
(ImageEnum.PODMAN, NodeType.PODMAN, "Podman", None),
] ]
for image_enum, node_type, label, model in nodes: for image_enum, node_type, label, model in nodes:
node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) node_draw = NodeDraw.from_setup(image_enum, node_type, label, model)

271
daemon/core/nodes/podman.py Normal file
View file

@ -0,0 +1,271 @@
import json
import logging
import shlex
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING
from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError, CoreError
from core.executables import BASH
from core.nodes.base import CoreNode, CoreNodeOptions
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.emulator.session import Session
PODMAN: str = "podman"
@dataclass
class PodmanOptions(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 VolumeMount:
src: str
"""volume mount name"""
dst: str
"""volume mount destination directory"""
unique: bool = True
"""True to create a node unique prefixed name for this volume"""
delete: bool = True
"""True to delete the volume during shutdown"""
path: str = None
"""path to the volume on the host"""
class PodmanNode(CoreNode):
"""
Provides logic for creating a Podman based node.
"""
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
server: DistributedServer = None,
options: PodmanOptions = None,
) -> None:
"""
Create a PodmanNode instance.
:param session: core session instance
:param _id: node id
:param name: node name
:param server: remote server node
will run on, default is None for localhost
:param options: options for creating node
"""
options = options or PodmanOptions()
super().__init__(session, _id, name, server, options)
self.image: str = options.image
self.binds: list[tuple[str, str]] = options.binds
self.volumes: dict[str, VolumeMount] = {}
for src, dst, unique, delete in options.volumes:
src_name = self._unique_name(src) if unique else src
self.volumes[src] = VolumeMount(src_name, dst, unique, delete)
@classmethod
def create_options(cls) -> PodmanOptions:
"""
Return default creation options, which can be used during node creation.
:return: podman options
"""
return PodmanOptions()
def create_cmd(self, args: str, shell: bool = False) -> str:
"""
Create command used to run commands within the context of a node.
:param args: command arguments
:param shell: True to run shell like, False otherwise
:return: node command
"""
if shell:
args = f"{BASH} -c {shlex.quote(args)}"
return f"{PODMAN} exec {self.name} {args}"
def _unique_name(self, name: str) -> str:
"""
Creates a session/node unique prefixed name for the provided input.
:param name: name to make unique
:return: unique session/node prefixed name
"""
return f"{self.session.id}.{self.id}.{name}"
def alive(self) -> bool:
"""
Check if the node is alive.
:return: True if node is alive, False otherwise
"""
try:
running = self.host_cmd(
f"{PODMAN} inspect -f '{{{{.State.Running}}}}' {self.name}"
)
return json.loads(running)
except CoreCommandError:
return False
def startup(self) -> None:
"""
Create a podman container instance for the specified image.
:return: nothing
"""
with self.lock:
if self.up:
raise CoreError(f"starting node({self.name}) that is already up")
# create node directory
self.makenodedir()
# setup commands for creating bind/volume mounts
binds = ""
for src, dst in self.binds:
binds += f"--mount type=bind,source={src},target={dst} "
volumes = ""
for volume in self.volumes.values():
volumes += (
f"--mount type=volume," f"source={volume.src},target={volume.dst} "
)
# normalize hostname
hostname = self.name.replace("_", "-")
# create container and retrieve the created containers PID
self.host_cmd(
f"{PODMAN} run -td --init --net=none --hostname {hostname} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"{binds} {volumes} "
f"--privileged {self.image} tail -f /dev/null"
)
# retrieve pid and process environment for use in nsenter commands
self.pid = self.host_cmd(
f"{PODMAN} inspect -f '{{{{.State.Pid}}}}' {self.name}"
)
# setup symlinks for bind and volume mounts within
for src, dst in self.binds:
link_path = self.host_path(Path(dst), True)
self.host_cmd(f"ln -s {src} {link_path}")
for volume in self.volumes.values():
volume.path = self.host_cmd(
f"{PODMAN} volume inspect -f '{{{{.Mountpoint}}}}' {volume.src}"
)
link_path = self.host_path(Path(volume.dst), True)
self.host_cmd(f"ln -s {volume.path} {link_path}")
logger.debug("node(%s) pid: %s", self.name, self.pid)
self.up = True
def shutdown(self) -> None:
"""
Shutdown logic.
:return: nothing
"""
# nothing to do if node is not up
if not self.up:
return
with self.lock:
self.ifaces.clear()
self.host_cmd(f"{PODMAN} rm -f {self.name}")
for volume in self.volumes.values():
if volume.delete:
self.host_cmd(f"{PODMAN} volume rm {volume.src}")
self.up = False
def termcmdstring(self, sh: str = "/bin/sh") -> str:
"""
Create a terminal command string.
:param sh: shell to execute command in
:return: str
"""
terminal = f"{PODMAN} exec -it {self.name} {sh}"
if self.server is None:
return terminal
else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def create_dir(self, dir_path: Path) -> None:
"""
Create a private directory.
:param dir_path: path to create
:return: nothing
"""
logger.debug("creating node dir: %s", dir_path)
self.cmd(f"mkdir -p {dir_path}")
def mount(self, src_path: str, target_path: str) -> None:
"""
Create and mount a directory.
:param src_path: source directory to mount
:param target_path: target directory to create
:return: nothing
:raises CoreCommandError: when a non-zero exit status occurs
"""
logger.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported")
def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
"""
Create a node file with a given mode.
:param file_path: name of file to create
:param contents: contents of file
:param mode: mode for file
:return: nothing
"""
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode())
temp.close()
temp_path = Path(temp.name)
directory = file_path.parent
if str(directory) != ".":
self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None:
self.server.remote_put(temp_path, temp_path)
self.host_cmd(f"{PODMAN} cp {temp_path} {self.name}:{file_path}")
self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None:
self.host_cmd(f"rm -f {temp_path}")
temp_path.unlink()
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
"""
Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified.
:param dst_path: file name to copy file to
:param src_path: file to copy
:param mode: mode to copy to
:return: nothing
"""
logger.info(
"node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0
)
self.cmd(f"mkdir -p {dst_path.parent}")
if self.server:
temp = NamedTemporaryFile(delete=False)
temp_path = Path(temp.name)
src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.host_cmd(f"{PODMAN} cp {src_path} {self.name}:{dst_path}")
if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}")

View file

@ -17,6 +17,7 @@ from core.nodes.docker import DockerNode, DockerOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode, LxcOptions from core.nodes.lxd import LxcNode, LxcOptions
from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode
from core.nodes.podman import PodmanNode, PodmanOptions
from core.nodes.wireless import WirelessNode from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -225,6 +226,9 @@ class DeviceElement(NodeElement):
elif isinstance(self.node, LxcNode): elif isinstance(self.node, LxcNode):
clazz = "lxc" clazz = "lxc"
image = self.node.image image = self.node.image
elif isinstance(self.node, PodmanNode):
clazz = "podman"
image = self.node.image
add_attribute(self.element, "class", clazz) add_attribute(self.element, "class", clazz)
add_attribute(self.element, "image", image) add_attribute(self.element, "image", image)
@ -808,6 +812,8 @@ class CoreXmlReader:
node_type = NodeTypes.DOCKER node_type = NodeTypes.DOCKER
elif clazz == "lxc": elif clazz == "lxc":
node_type = NodeTypes.LXC node_type = NodeTypes.LXC
elif clazz == "podman":
node_type = NodeTypes.PODMAN
_class = self.session.get_node_class(node_type) _class = self.session.get_node_class(node_type)
options = _class.create_options() options = _class.create_options()
options.icon = icon options.icon = icon
@ -825,7 +831,7 @@ class CoreXmlReader:
options.config_services.extend( options.config_services.extend(
x.get("name") for x in config_service_elements.iterchildren() x.get("name") for x in config_service_elements.iterchildren()
) )
if isinstance(options, (DockerOptions, LxcOptions)): if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
options.image = image options.image = image
# get position information # get position information
position_element = device_element.find("position") position_element = device_element.find("position")

View file

@ -545,6 +545,8 @@ message NodeType {
CONTROL_NET = 13; CONTROL_NET = 13;
DOCKER = 15; DOCKER = 15;
LXC = 16; LXC = 16;
WIRELESS = 17;
PODMAN = 18;
} }
} }