daemon: cleaned up docker node code, updates to use nsenter in most cases

This commit is contained in:
Blake Harnden 2022-05-05 21:54:08 -07:00
parent 2e4d0e0cea
commit 2094694ca3

View file

@ -1,75 +1,28 @@
import json import json
import logging import logging
import shlex
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING
from core import utils
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError from core.errors import CoreCommandError, CoreError
from core.executables import BASH
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
DOCKER: str = "docker"
class DockerClient:
def __init__(self, name: str, image: str, run: Callable[..., str]) -> None:
self.name: str = name
self.image: str = image
self.run: Callable[..., str] = run
self.pid: Optional[str] = None
def create_container(self) -> str:
self.run(
f"docker run -td --init --net=none --hostname {self.name} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"--privileged {self.image} /bin/bash"
)
self.pid = self.get_pid()
return self.pid
def get_info(self) -> Dict:
args = f"docker inspect {self.name}"
output = self.run(args)
data = json.loads(output)
if not data:
raise CoreCommandError(1, args, f"docker({self.name}) not present")
return data[0]
def is_alive(self) -> bool:
try:
data = self.get_info()
return data["State"]["Running"]
except CoreCommandError:
return False
def stop_container(self) -> None:
self.run(f"docker rm -f {self.name}")
def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str:
logger.info("docker cmd output: %s", cmd)
return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell)
def create_ns_cmd(self, cmd: str) -> str:
return f"nsenter -t {self.pid} -a {cmd}"
def get_pid(self) -> str:
args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}"
output = self.run(args)
self.pid = output
logger.debug("node(%s) pid: %s", self.name, self.pid)
return output
def copy_file(self, src_path: Path, dst_path: Path) -> str:
args = f"docker cp {src_path} {self.name}:{dst_path}"
return self.run(args)
class DockerNode(CoreNode): class DockerNode(CoreNode):
"""
Provides logic for creating a Docker based node.
"""
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
@ -92,17 +45,18 @@ class DockerNode(CoreNode):
""" """
super().__init__(session, _id, name, directory, server) super().__init__(session, _id, name, directory, server)
self.image = image if image is not None else "ubuntu" self.image = image if image is not None else "ubuntu"
self.client: Optional[DockerClient] = None
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: def _create_cmd(self, args: str, shell: bool = False) -> str:
""" """
Create node network client for running network commands within the nodes Create command used to run commands within the context of a node.
container.
:param use_ovs: True for OVS bridges, False for Linux bridges :param args: command arguments
:return:node network client :param shell: True to run shell like, False otherwise
:return: node command
""" """
return get_net_client(use_ovs, self.nsenter_cmd) if shell:
args = f"{BASH} -c {shlex.quote(args)}"
return f"nsenter -t {self.pid} -m -u -i -p -n {args}"
def alive(self) -> bool: def alive(self) -> bool:
""" """
@ -110,22 +64,33 @@ class DockerNode(CoreNode):
:return: True if node is alive, False otherwise :return: True if node is alive, False otherwise
""" """
return self.client.is_alive() try:
running = self.host_cmd(
f"{DOCKER} inspect -f '{{{{.State.Running}}}}' {self.name}"
)
return json.loads(running)
except CoreCommandError:
return False
def startup(self) -> None: def startup(self) -> None:
""" """
Start a new namespace node by invoking the vnoded process that Create a docker container instance for the specified image.
allocates a new namespace. Bring up the loopback device and set
the hostname.
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
if self.up: if self.up:
raise ValueError("starting a node that is already up") raise CoreError(f"starting node({self.name}) that is already up")
self.makenodedir() self.makenodedir()
self.client = DockerClient(self.name, self.image, self.host_cmd) self.host_cmd(
self.pid = self.client.create_container() f"{DOCKER} run -td --init --net=none --hostname {self.name} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"--privileged {self.image} /bin/bash"
)
self.pid = self.host_cmd(
f"{DOCKER} inspect -f '{{{{.State.Pid}}}}' {self.name}"
)
logger.debug("node(%s) pid: %s", self.name, self.pid)
self.up = True self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
@ -139,17 +104,9 @@ class DockerNode(CoreNode):
return return
with self.lock: with self.lock:
self.ifaces.clear() self.ifaces.clear()
self.client.stop_container() self.host_cmd(f"{DOCKER} rm -f {self.name}")
self.up = False self.up = False
def nsenter_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
if self.server is None:
args = self.client.create_ns_cmd(args)
return utils.cmd(args, wait=wait, shell=shell)
else:
args = self.client.create_ns_cmd(args)
return self.server.remote_cmd(args, wait=wait)
def termcmdstring(self, sh: str = "/bin/sh") -> str: def termcmdstring(self, sh: str = "/bin/sh") -> str:
""" """
Create a terminal command string. Create a terminal command string.
@ -157,7 +114,7 @@ class DockerNode(CoreNode):
:param sh: shell to execute command in :param sh: shell to execute command in
:return: str :return: str
""" """
return f"docker exec -it {self.name} bash" return f"{DOCKER} exec -it {self.name} {sh}"
def create_dir(self, dir_path: Path) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
@ -167,8 +124,7 @@ class DockerNode(CoreNode):
:return: nothing :return: nothing
""" """
logger.debug("creating node dir: %s", dir_path) logger.debug("creating node dir: %s", dir_path)
args = f"mkdir -p {dir_path}" self.cmd(f"mkdir -p {dir_path}")
self.cmd(args)
def mount(self, src_path: str, target_path: str) -> None: def mount(self, src_path: str, target_path: str) -> None:
""" """
@ -201,7 +157,7 @@ class DockerNode(CoreNode):
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:
self.server.remote_put(temp_path, temp_path) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp_path, file_path) self.host_cmd(f"{DOCKER} cp {temp_path} {self.name}:{file_path}")
self.cmd(f"chmod {mode:o} {file_path}") self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None: if self.server is not None:
self.host_cmd(f"rm -f {temp_path}") self.host_cmd(f"rm -f {temp_path}")
@ -226,6 +182,6 @@ class DockerNode(CoreNode):
temp_path = Path(temp.name) temp_path = Path(temp.name)
src_path = temp_path src_path = temp_path
self.server.remote_put(src_path, temp_path) self.server.remote_put(src_path, temp_path)
self.client.copy_file(src_path, dst_path) self.host_cmd(f"{DOCKER} cp {src_path} {self.name}:{dst_path}")
if mode is not None: if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}") self.cmd(f"chmod {mode:o} {dst_path}")