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
"""
request = GetConfigServiceDefaultsRequest(
name=name,
session_id=session_id,
node_id=node_id,
name=name, session_id=session_id, node_id=node_id
)
response = self.stub.GetConfigServiceDefaults(request)
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.lxd import LxcNode, LxcOptions
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
from core.nodes.podman import PodmanNode, PodmanOptions
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService
@ -81,7 +82,7 @@ def add_node_data(
options.config_services = node_proto.config_services
if isinstance(options, EmaneOptions):
options.emane_model = node_proto.emane
if isinstance(options, (DockerOptions, LxcOptions)):
if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
options.image = node_proto.image
position = Position()
position.set(node_proto.position.x, node_proto.position.y)
@ -313,7 +314,7 @@ def get_node_proto(
if isinstance(node, EmaneNet):
emane_model = node.wireless_model.name
image = None
if isinstance(node, (DockerNode, LxcNode)):
if isinstance(node, (DockerNode, LxcNode, PodmanNode)):
image = node.image
# check for wlan config
wlan_config = session.mobility.get_configs(

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ class HookManager:
state_hooks = self.script_hooks.setdefault(state, {})
if file_name in state_hooks:
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
@ -59,7 +59,7 @@ class HookManager:
if file_name not in state_hooks:
raise CoreError(
f"deleting state({state.name}) hook script({file_name}) "
"that does not exist",
"that does not exist"
)
del state_hooks[file_name]
@ -77,7 +77,7 @@ class HookManager:
if hook in hooks:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
f"adding duplicate state({state.name}) hook callback({name})",
f"adding duplicate state({state.name}) hook callback({name})"
)
hooks.append(hook)
@ -96,7 +96,7 @@ class HookManager:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
f"deleting state({state.name}) hook callback({name}) "
"that does not exist",
"that does not exist"
)
hooks.remove(hook)
@ -132,7 +132,7 @@ class HookManager:
except (OSError, subprocess.CalledProcessError) as e:
raise CoreError(
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, []):
try:
@ -141,5 +141,5 @@ class HookManager:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
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,
)
from core.nodes.physical import PhysicalNode, Rj45Node
from core.nodes.podman import PodmanNode
from core.nodes.wireless import WirelessNode
from core.plugins.sdt import Sdt
from core.services.coreservices import CoreServices
@ -81,9 +82,9 @@ NODES: dict[NodeTypes, type[NodeBase]] = {
NodeTypes.DOCKER: DockerNode,
NodeTypes.LXC: LxcNode,
NodeTypes.WIRELESS: WirelessNode,
NodeTypes.PODMAN: PodmanNode,
}
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
LINK_COLORS: list[str] = ["green", "blue", "orange", "purple", "turquoise"]
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)
self.red_label = ttk.Label(
frame,
background=get_rgb(self.red.get(), 0, 0),
width=5,
frame, background=get_rgb(self.red.get(), 0, 0), width=5
)
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)
self.green_label = ttk.Label(
frame,
background=get_rgb(0, self.green.get(), 0),
width=5,
frame, background=get_rgb(0, self.green.get(), 0), width=5
)
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)
self.blue_label = ttk.Label(
frame,
background=get_rgb(0, 0, self.blue.get()),
width=5,
frame, background=get_rgb(0, 0, self.blue.get()), width=5
)
self.blue_label.grid(row=0, column=3, sticky=tk.EW)

View file

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

View file

@ -16,8 +16,13 @@ if TYPE_CHECKING:
NODES: list["NodeDraw"] = []
NETWORK_NODES: list["NodeDraw"] = []
NODE_ICONS = {}
CONTAINER_NODES: set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
CONTAINER_NODES: set[NodeType] = {
NodeType.DEFAULT,
NodeType.DOCKER,
NodeType.LXC,
NodeType.PODMAN,
}
IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC, NodeType.PODMAN}
WIRELESS_NODES: set[NodeType] = {
NodeType.WIRELESS_LAN,
NodeType.EMANE,
@ -41,6 +46,7 @@ def setup() -> None:
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
(ImageEnum.LXC, NodeType.LXC, "LXC", None),
(ImageEnum.PODMAN, NodeType.PODMAN, "Podman", None),
]
for image_enum, node_type, label, model in nodes:
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.lxd import LxcNode, LxcOptions
from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode
from core.nodes.podman import PodmanNode, PodmanOptions
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService
@ -225,6 +226,9 @@ class DeviceElement(NodeElement):
elif isinstance(self.node, LxcNode):
clazz = "lxc"
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, "image", image)
@ -808,6 +812,8 @@ class CoreXmlReader:
node_type = NodeTypes.DOCKER
elif clazz == "lxc":
node_type = NodeTypes.LXC
elif clazz == "podman":
node_type = NodeTypes.PODMAN
_class = self.session.get_node_class(node_type)
options = _class.create_options()
options.icon = icon
@ -825,7 +831,7 @@ class CoreXmlReader:
options.config_services.extend(
x.get("name") for x in config_service_elements.iterchildren()
)
if isinstance(options, (DockerOptions, LxcOptions)):
if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
options.image = image
# get position information
position_element = device_element.find("position")

View file

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