Merge pull request #700 from coreemu/enhancement/opt-core

Enhancement/opt core
This commit is contained in:
bharnden 2022-08-30 12:08:28 -07:00 committed by GitHub
commit 5202b2fa04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 592 additions and 188 deletions

View file

103
daemon/core/scripts/cleanup.py Executable file
View file

@ -0,0 +1,103 @@
import argparse
import os
import subprocess
import sys
import time
def check_root() -> None:
if os.geteuid() != 0:
print("permission denied, run this script as root")
sys.exit(1)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="helps cleanup lingering core processes and files",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-d", "--daemon", action="store_true", help="also kill core-daemon"
)
return parser.parse_args()
def cleanup_daemon() -> None:
print("killing core-daemon process ... ", end="")
result = subprocess.call("pkill -9 core-daemon", shell=True)
if result:
print("not found")
else:
print("done")
def cleanup_nodes() -> None:
print("killing vnoded processes ... ", end="")
result = subprocess.call("pkill -KILL vnoded", shell=True)
if result:
print("none found")
else:
time.sleep(1)
print("done")
def cleanup_emane() -> None:
print("killing emane processes ... ", end="")
result = subprocess.call("pkill emane", shell=True)
if result:
print("none found")
else:
print("done")
def cleanup_sessions() -> None:
print("removing session directories ... ", end="")
result = subprocess.call("rm -rf /tmp/pycore*", shell=True)
if result:
print("none found")
else:
print("done")
def cleanup_interfaces() -> None:
print("cleaning up devices")
output = subprocess.check_output("ip -o -br link show", shell=True)
lines = output.decode().strip().split("\n")
for line in lines:
values = line.split()
name = values[0]
if (
name.startswith("veth")
or name.startswith("gt.")
or name.startswith("b.")
or name.startswith("ctrl")
):
result = subprocess.call(f"ip link delete {name}", shell=True)
if result:
print(f"failed to remove {name}")
else:
print(f"removed {name}")
if name.startswith("b."):
result = subprocess.call(
f"nft delete table bridge {name}",
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
shell=True,
)
if not result:
print(f"cleared nft rules for {name}")
def main() -> None:
check_root()
args = parse_args()
if args.daemon:
cleanup_daemon()
cleanup_nodes()
cleanup_emane()
cleanup_interfaces()
cleanup_sessions()
if __name__ == "__main__":
main()

628
daemon/core/scripts/cli.py Executable file
View file

@ -0,0 +1,628 @@
import json
import sys
from argparse import (
ArgumentDefaultsHelpFormatter,
ArgumentParser,
ArgumentTypeError,
Namespace,
)
from functools import wraps
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
import grpc
import netaddr
from google.protobuf.json_format import MessageToDict
from netaddr import EUI, AddrFormatError, IPNetwork
from core.api.grpc.client import CoreGrpcClient
from core.api.grpc.wrappers import (
ConfigOption,
Geo,
Interface,
Link,
LinkOptions,
Node,
NodeType,
Position,
)
NODE_TYPES = [x.name for x in NodeType if x != NodeType.PEER_TO_PEER]
def protobuf_to_json(message: Any) -> Dict[str, Any]:
return MessageToDict(
message, including_default_value_fields=True, preserving_proto_field_name=True
)
def print_json(data: Any) -> None:
data = json.dumps(data, indent=2)
print(data)
def coreclient(func):
@wraps(func)
def wrapper(*args, **kwargs):
core = CoreGrpcClient()
try:
with core.context_connect():
return func(core, *args, **kwargs)
except grpc.RpcError as e:
print(f"grpc error: {e.details()}")
return wrapper
def mac_type(value: str) -> str:
try:
mac = EUI(value, dialect=netaddr.mac_unix_expanded)
return str(mac)
except AddrFormatError:
raise ArgumentTypeError(f"invalid mac address: {value}")
def ip4_type(value: str) -> IPNetwork:
try:
ip = IPNetwork(value)
if not netaddr.valid_ipv4(str(ip.ip)):
raise ArgumentTypeError(f"invalid ip4 address: {value}")
return ip
except AddrFormatError:
raise ArgumentTypeError(f"invalid ip4 address: {value}")
def ip6_type(value: str) -> IPNetwork:
try:
ip = IPNetwork(value)
if not netaddr.valid_ipv6(str(ip.ip)):
raise ArgumentTypeError(f"invalid ip6 address: {value}")
return ip
except AddrFormatError:
raise ArgumentTypeError(f"invalid ip6 address: {value}")
def position_type(value: str) -> Tuple[float, float]:
error = "invalid position, must be in the format: float,float"
try:
values = [float(x) for x in value.split(",")]
except ValueError:
raise ArgumentTypeError(error)
if len(values) != 2:
raise ArgumentTypeError(error)
x, y = values
return x, y
def geo_type(value: str) -> Tuple[float, float, float]:
error = "invalid geo, must be in the format: float,float,float"
try:
values = [float(x) for x in value.split(",")]
except ValueError:
raise ArgumentTypeError(error)
if len(values) != 3:
raise ArgumentTypeError(error)
lon, lat, alt = values
return lon, lat, alt
def file_type(value: str) -> Path:
path = Path(value)
if not path.is_file():
raise ArgumentTypeError(f"invalid file: {value}")
return path
def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
if session_id:
return session_id
sessions = core.get_sessions()
if not sessions:
print("no current session to interact with")
sys.exit(1)
return sessions[0].id
def create_iface(
iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork
) -> Interface:
ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None
ip6 = str(ip6_net.ip) if ip6_net else None
ip6_mask = ip6_net.prefixlen if ip6_net else None
return Interface(
id=iface_id, mac=mac, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask
)
def print_iface_header() -> None:
print("ID | MAC Address | IP4 Address | IP6 Address")
def print_iface(iface: Interface) -> None:
iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else ""
iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else ""
print(f"{iface.id:<3} | {iface.mac:<17} | {iface_ip4:<18} | {iface_ip6}")
@coreclient
def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
config = core.get_wlan_config(session_id, args.node)
if args.json:
print_json(ConfigOption.to_dict(config))
else:
size = 0
for option in config.values():
size = max(size, len(option.name))
print(f"{'Name':<{size}.{size}} | Value")
for option in config.values():
print(f"{option.name:<{size}.{size}} | {option.value}")
@coreclient
def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
config = {}
if args.bandwidth:
config["bandwidth"] = str(args.bandwidth)
if args.delay:
config["delay"] = str(args.delay)
if args.loss:
config["error"] = str(args.loss)
if args.jitter:
config["jitter"] = str(args.jitter)
if args.range:
config["range"] = str(args.range)
result = core.set_wlan_config(session_id, args.node, config)
if args.json:
print_json(dict(result=result))
else:
print(f"set wlan config: {result}")
@coreclient
def open_xml(core: CoreGrpcClient, args: Namespace) -> None:
result, session_id = core.open_xml(args.file, args.start)
if args.json:
print_json(dict(result=result, session_id=session_id))
else:
print(f"opened xml: {result},{session_id}")
@coreclient
def query_sessions(core: CoreGrpcClient, args: Namespace) -> None:
sessions = core.get_sessions()
if args.json:
sessions = [protobuf_to_json(x.to_proto()) for x in sessions]
print_json(sessions)
else:
print("Session ID | Session State | Nodes")
for session in sessions:
print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}")
@coreclient
def query_session(core: CoreGrpcClient, args: Namespace) -> None:
session = core.get_session(args.id)
if args.json:
session = protobuf_to_json(session.to_proto())
print_json(session)
else:
print("Nodes")
print("ID | Name | Type | XY | Geo")
for node in session.nodes.values():
xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print(
f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}"
)
print("\nLinks")
for link in session.links:
n1 = session.nodes[link.node1_id].name
n2 = session.nodes[link.node2_id].name
print("Node | ", end="")
print_iface_header()
print(f"{n1:<6} | ", end="")
if link.iface1:
print_iface(link.iface1)
else:
print()
print(f"{n2:<6} | ", end="")
if link.iface2:
print_iface(link.iface2)
else:
print()
print()
@coreclient
def query_node(core: CoreGrpcClient, args: Namespace) -> None:
session = core.get_session(args.id)
node, ifaces, _ = core.get_node(args.id, args.node)
if args.json:
node = protobuf_to_json(node.to_proto())
ifaces = [protobuf_to_json(x.to_proto()) for x in ifaces]
print_json(dict(node=node, ifaces=ifaces))
else:
print("ID | Name | Type | XY | Geo")
xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print(
f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}"
)
if ifaces:
print("Interfaces")
print("Connected To | ", end="")
print_iface_header()
for iface in ifaces:
if iface.net_id == node.id:
if iface.node_id:
name = session.nodes[iface.node_id].name
else:
name = session.nodes[iface.net2_id].name
else:
net_node = session.nodes.get(iface.net_id)
name = net_node.name if net_node else ""
print(f"{name:<12} | ", end="")
print_iface(iface)
@coreclient
def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
result = core.delete_session(args.id)
if args.json:
print_json(dict(result=result))
else:
print(f"delete session({args.id}): {result}")
@coreclient
def add_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
node_type = NodeType[args.type]
pos = None
if args.pos:
x, y = args.pos
pos = Position(x=x, y=y)
geo = None
if args.geo:
lon, lat, alt = args.geo
geo = Geo(lon=lon, lat=lat, alt=alt)
node = Node(
id=args.id,
name=args.name,
type=node_type,
model=args.model,
emane=args.emane,
icon=args.icon,
image=args.image,
position=pos,
geo=geo,
)
node_id = core.add_node(session_id, node)
if args.json:
print_json(dict(node_id=node_id))
else:
print(f"created node: {node_id}")
@coreclient
def edit_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
result = core.edit_node(session_id, args.id, args.icon)
if args.json:
print_json(dict(result=result))
else:
print(f"edit node: {result}")
@coreclient
def move_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
pos = None
if args.pos:
x, y = args.pos
pos = Position(x=x, y=y)
geo = None
if args.geo:
lon, lat, alt = args.geo
geo = Geo(lon=lon, lat=lat, alt=alt)
result = core.move_node(session_id, args.id, pos, geo)
if args.json:
print_json(dict(result=result))
else:
print(f"move node: {result}")
@coreclient
def delete_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
result = core.delete_node(session_id, args.id)
if args.json:
print_json(dict(result=result))
else:
print(f"deleted node: {result}")
@coreclient
def add_link(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
iface1 = None
if args.iface1_id is not None:
iface1 = create_iface(
args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6
)
iface2 = None
if args.iface2_id is not None:
iface2 = create_iface(
args.iface2_id, args.iface2_mac, args.iface2_ip4, args.iface2_ip6
)
options = LinkOptions(
bandwidth=args.bandwidth,
loss=args.loss,
jitter=args.jitter,
delay=args.delay,
dup=args.duplicate,
unidirectional=args.uni,
)
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
result, iface1, iface2 = core.add_link(session_id, link)
if args.json:
iface1 = protobuf_to_json(iface1.to_proto())
iface2 = protobuf_to_json(iface2.to_proto())
print_json(dict(result=result, iface1=iface1, iface2=iface2))
else:
print(f"add link: {result}")
@coreclient
def edit_link(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
options = LinkOptions(
bandwidth=args.bandwidth,
loss=args.loss,
jitter=args.jitter,
delay=args.delay,
dup=args.duplicate,
unidirectional=args.uni,
)
iface1 = Interface(args.iface1)
iface2 = Interface(args.iface2)
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
result = core.edit_link(session_id, link)
if args.json:
print_json(dict(result=result))
else:
print(f"edit link: {result}")
@coreclient
def delete_link(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
iface1 = Interface(args.iface1)
iface2 = Interface(args.iface2)
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2)
result = core.delete_link(session_id, link)
if args.json:
print_json(dict(result=result))
else:
print(f"delete link: {result}")
def setup_sessions_parser(parent) -> None:
parser = parent.add_parser("session", help="session interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True)
subparsers = parser.add_subparsers(help="session commands")
subparsers.required = True
subparsers.dest = "command"
delete_parser = subparsers.add_parser("delete", help="delete a session")
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
delete_parser.set_defaults(func=delete_session)
def setup_node_parser(parent) -> None:
parser = parent.add_parser("node", help="node interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with")
subparsers = parser.add_subparsers(help="node commands")
subparsers.required = True
subparsers.dest = "command"
add_parser = subparsers.add_parser("add", help="add a node")
add_parser.formatter_class = ArgumentDefaultsHelpFormatter
add_parser.add_argument("-i", "--id", type=int, help="id to use, optional")
add_parser.add_argument("-n", "--name", help="name to use, optional")
add_parser.add_argument(
"-t", "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node"
)
add_parser.add_argument(
"-m", "--model", help="used to determine services, optional"
)
group = add_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-p", "--pos", type=position_type, help="x,y position")
group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position")
add_parser.add_argument("-ic", "--icon", help="icon to use, optional")
add_parser.add_argument("-im", "--image", help="container image, optional")
add_parser.add_argument(
"-e", "--emane", help="emane model, only required for emane nodes"
)
add_parser.set_defaults(func=add_node)
edit_parser = subparsers.add_parser("edit", help="edit a node")
edit_parser.formatter_class = ArgumentDefaultsHelpFormatter
edit_parser.add_argument("-i", "--id", type=int, help="id to use", required=True)
edit_parser.add_argument("-ic", "--icon", help="icon to use, optional")
edit_parser.set_defaults(func=edit_node)
move_parser = subparsers.add_parser("move", help="move a node")
move_parser.formatter_class = ArgumentDefaultsHelpFormatter
move_parser.add_argument(
"-i", "--id", type=int, help="id to use, optional", required=True
)
group = move_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-p", "--pos", type=position_type, help="x,y position")
group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position")
move_parser.set_defaults(func=move_node)
delete_parser = subparsers.add_parser("delete", help="delete a node")
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
delete_parser.add_argument("-i", "--id", type=int, help="node id", required=True)
delete_parser.set_defaults(func=delete_node)
def setup_link_parser(parent) -> None:
parser = parent.add_parser("link", help="link interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with")
subparsers = parser.add_subparsers(help="link commands")
subparsers.required = True
subparsers.dest = "command"
add_parser = subparsers.add_parser("add", help="add a node")
add_parser.formatter_class = ArgumentDefaultsHelpFormatter
add_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True)
add_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True)
add_parser.add_argument("-i1-i", "--iface1-id", type=int, help="node1 interface id")
add_parser.add_argument(
"-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac"
)
add_parser.add_argument(
"-i1-4", "--iface1-ip4", type=ip4_type, help="node1 interface ip4"
)
add_parser.add_argument(
"-i1-6", "--iface1-ip6", type=ip6_type, help="node1 interface ip6"
)
add_parser.add_argument("-i2-i", "--iface2-id", type=int, help="node2 interface id")
add_parser.add_argument(
"-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac"
)
add_parser.add_argument(
"-i2-4", "--iface2-ip4", type=ip4_type, help="node2 interface ip4"
)
add_parser.add_argument(
"-i2-6", "--iface2-ip6", type=ip6_type, help="node2 interface ip6"
)
add_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)")
add_parser.add_argument("-l", "--loss", type=float, help="loss (%%)")
add_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)")
add_parser.add_argument("-de", "--delay", type=int, help="delay (us)")
add_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)")
add_parser.add_argument(
"-u", "--uni", action="store_true", help="is link unidirectional?"
)
add_parser.set_defaults(func=add_link)
edit_parser = subparsers.add_parser("edit", help="edit a link")
edit_parser.formatter_class = ArgumentDefaultsHelpFormatter
edit_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True)
edit_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True)
edit_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id")
edit_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id")
edit_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)")
edit_parser.add_argument("-l", "--loss", type=float, help="loss (%%)")
edit_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)")
edit_parser.add_argument("-de", "--delay", type=int, help="delay (us)")
edit_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)")
edit_parser.add_argument(
"-u", "--uni", action="store_true", help="is link unidirectional?"
)
edit_parser.set_defaults(func=edit_link)
delete_parser = subparsers.add_parser("delete", help="delete a link")
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
delete_parser.add_argument(
"-n1", "--node1", type=int, help="node1 id", required=True
)
delete_parser.add_argument(
"-n2", "--node2", type=int, help="node1 id", required=True
)
delete_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id")
delete_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id")
delete_parser.set_defaults(func=delete_link)
def setup_query_parser(parent) -> None:
parser = parent.add_parser("query", help="query interactions")
subparsers = parser.add_subparsers(help="query commands")
subparsers.required = True
subparsers.dest = "command"
sessions_parser = subparsers.add_parser("sessions", help="query current sessions")
sessions_parser.formatter_class = ArgumentDefaultsHelpFormatter
sessions_parser.set_defaults(func=query_sessions)
session_parser = subparsers.add_parser("session", help="query session")
session_parser.formatter_class = ArgumentDefaultsHelpFormatter
session_parser.add_argument(
"-i", "--id", type=int, help="session to query", required=True
)
session_parser.set_defaults(func=query_session)
node_parser = subparsers.add_parser("node", help="query node")
node_parser.formatter_class = ArgumentDefaultsHelpFormatter
node_parser.add_argument(
"-i", "--id", type=int, help="session to query", required=True
)
node_parser.add_argument(
"-n", "--node", type=int, help="node to query", required=True
)
node_parser.set_defaults(func=query_node)
def setup_xml_parser(parent) -> None:
parser = parent.add_parser("xml", help="open session xml")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument(
"-f", "--file", type=file_type, help="xml file to open", required=True
)
parser.add_argument("-s", "--start", action="store_true", help="start the session?")
parser.set_defaults(func=open_xml)
def setup_wlan_parser(parent) -> None:
parser = parent.add_parser("wlan", help="wlan specific interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with")
subparsers = parser.add_subparsers(help="link commands")
subparsers.required = True
subparsers.dest = "command"
get_parser = subparsers.add_parser("get", help="get wlan configuration")
get_parser.formatter_class = ArgumentDefaultsHelpFormatter
get_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True)
get_parser.set_defaults(func=get_wlan_config)
set_parser = subparsers.add_parser("set", help="set wlan configuration")
set_parser.formatter_class = ArgumentDefaultsHelpFormatter
set_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True)
set_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)")
set_parser.add_argument("-d", "--delay", type=int, help="delay (us)")
set_parser.add_argument("-l", "--loss", type=float, help="loss (%%)")
set_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)")
set_parser.add_argument("-r", "--range", type=int, help="range (pixels)")
set_parser.set_defaults(func=set_wlan_config)
def main() -> None:
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument(
"-js", "--json", action="store_true", help="print responses to terminal as json"
)
subparsers = parser.add_subparsers(help="supported commands")
subparsers.required = True
subparsers.dest = "command"
setup_sessions_parser(subparsers)
setup_node_parser(subparsers)
setup_link_parser(subparsers)
setup_query_parser(subparsers)
setup_xml_parser(subparsers)
setup_wlan_parser(subparsers)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

130
daemon/core/scripts/daemon.py Executable file
View file

@ -0,0 +1,130 @@
"""
core-daemon: the CORE daemon is a server process that receives CORE API
messages and instantiates emulated nodes and networks within the kernel. Various
message handlers are defined and some support for sending messages.
"""
import argparse
import logging
import os
import time
from configparser import ConfigParser
from pathlib import Path
from core import constants
from core.api.grpc.server import CoreGrpcServer
from core.constants import CORE_CONF_DIR, COREDPY_VERSION
from core.emulator.coreemu import CoreEmu
from core.utils import load_logging_config
logger = logging.getLogger(__name__)
def banner():
"""
Output the program banner printed to the terminal or log file.
:return: nothing
"""
logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime())
def cored(cfg):
"""
Start the CoreServer object and enter the server loop.
:param dict cfg: core configuration
:return: nothing
"""
# initialize grpc api
coreemu = CoreEmu(cfg)
grpc_server = CoreGrpcServer(coreemu)
address_config = cfg["grpcaddress"]
port_config = cfg["grpcport"]
grpc_address = f"{address_config}:{port_config}"
grpc_server.listen(grpc_address)
def get_merged_config(filename):
"""
Return a configuration after merging config file and command-line arguments.
:param str filename: file name to merge configuration settings with
:return: merged configuration
:rtype: dict
"""
# these are the defaults used in the config file
default_log = os.path.join(constants.CORE_CONF_DIR, "logging.conf")
default_grpc_port = "50051"
default_address = "localhost"
defaults = {
"grpcport": default_grpc_port,
"grpcaddress": default_address,
"logfile": default_log,
}
parser = argparse.ArgumentParser(
description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes."
)
parser.add_argument(
"-f",
"--configfile",
dest="configfile",
help=f"read config from specified file; default = {filename}",
)
parser.add_argument(
"--ovs",
action="store_true",
help="enable experimental ovs mode, default is false",
)
parser.add_argument(
"--grpc-port",
dest="grpcport",
help=f"grpc port to listen on; default {default_grpc_port}",
)
parser.add_argument(
"--grpc-address",
dest="grpcaddress",
help=f"grpc address to listen on; default {default_address}",
)
parser.add_argument(
"-l", "--logfile", help=f"core logging configuration; default {default_log}"
)
# parse command line options
args = parser.parse_args()
# convert ovs to internal format
args.ovs = "1" if args.ovs else "0"
# read the config file
if args.configfile is not None:
filename = args.configfile
del args.configfile
cfg = ConfigParser(defaults)
cfg.read(filename)
section = "core-daemon"
if not cfg.has_section(section):
cfg.add_section(section)
# merge argparse with configparser
for opt in vars(args):
val = getattr(args, opt)
if val is not None:
cfg.set(section, opt, str(val))
return dict(cfg.items(section))
def main():
"""
Main program startup.
:return: nothing
"""
cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf")
log_config_path = Path(cfg["logfile"])
load_logging_config(log_config_path)
banner()
try:
cored(cfg)
except KeyboardInterrupt:
logger.info("keyboard interrupt, stopping core daemon")
if __name__ == "__main__":
main()

50
daemon/core/scripts/gui.py Executable file
View file

@ -0,0 +1,50 @@
import argparse
import logging
from logging.handlers import TimedRotatingFileHandler
from core.gui import appconfig, images
from core.gui.app import Application
def main() -> None:
# parse flags
parser = argparse.ArgumentParser(description="CORE Python GUI")
parser.add_argument(
"-l",
"--level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="INFO",
help="logging level",
)
parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy")
parser.add_argument("-s", "--session", type=int, help="session id to join")
parser.add_argument(
"--create-dir", action="store_true", help="create gui directory and exit"
)
args = parser.parse_args()
# check home directory exists and create if necessary
appconfig.check_directory()
if args.create_dir:
return
# setup logging
log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
stream_handler = logging.StreamHandler()
file_handler = TimedRotatingFileHandler(
filename=appconfig.LOG_PATH, when="D", backupCount=5
)
log_level = logging.getLevelName(args.level)
logging.basicConfig(
level=log_level, format=log_format, handlers=[stream_handler, file_handler]
)
logging.getLogger("PIL").setLevel(logging.ERROR)
# start app
images.load_all()
app = Application(args.proxy, args.session)
app.mainloop()
if __name__ == "__main__":
main()

51
daemon/core/scripts/player.py Executable file
View file

@ -0,0 +1,51 @@
import argparse
import logging
import sys
from pathlib import Path
from core.player import CorePlayer
logger = logging.getLogger(__name__)
def path_type(value: str) -> Path:
file_path = Path(value)
if not file_path.is_file():
raise argparse.ArgumentTypeError(f"file does not exist: {value}")
return file_path
def parse_args() -> argparse.Namespace:
"""
Setup and parse command line arguments.
:return: parsed arguments
"""
parser = argparse.ArgumentParser(
description="core player runs files that can move nodes and send commands",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-f", "--file", required=True, type=path_type, help="core file to play"
)
parser.add_argument(
"-s",
"--session",
type=int,
help="session to play to, first found session otherwise",
)
return parser.parse_args()
def main() -> None:
logging.basicConfig(level=logging.INFO)
args = parse_args()
player = CorePlayer(args.file)
result = player.init(args.session)
if not result:
sys.exit(1)
player.start()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,259 @@
import argparse
import enum
import select
import socket
import subprocess
import sys
import time
from argparse import ArgumentDefaultsHelpFormatter
from functools import cmp_to_key
from queue import Queue
from threading import Thread
from typing import Dict, Tuple
import grpc
from core import utils
from core.api.grpc.client import CoreGrpcClient
from core.api.grpc.wrappers import NodeType
SDT_HOST = "127.0.0.1"
SDT_PORT = 50000
ROUTE_LAYER = "CORE Route"
DEAD_TIME = 3
ROUTE_TIME = 3
PACKET_CHOICES = ["udp", "tcp", "icmp"]
class RouteEnum(enum.Enum):
ADD = 0
DEL = 1
class SdtClient:
def __init__(self, address: Tuple[str, int]) -> None:
self.sock = socket.create_connection(address)
self.links = []
self.send(f'layer "{ROUTE_LAYER}"')
def close(self) -> None:
self.sock.close()
def send(self, cmd: str) -> None:
sdt_cmd = f"{cmd}\n".encode()
self.sock.sendall(sdt_cmd)
def add_link(self, node1, node2) -> None:
route_id = f"{node1}-{node2}-r"
link_id = f"{node1},{node2},{route_id}"
cmd = f'link {link_id} linkLayer "{ROUTE_LAYER}" line yellow,2'
self.send(cmd)
self.links.append(link_id)
def delete_links(self) -> None:
for link_id in self.links:
cmd = f"delete link,{link_id}"
self.send(cmd)
self.links.clear()
class RouterMonitor:
def __init__(
self,
session: int,
src: str,
dst: str,
pkt: str,
rate: int,
dead: int,
sdt_host: str,
sdt_port: int,
) -> None:
self.queue = Queue()
self.core = CoreGrpcClient()
self.session = session
self.src_id = None
self.src = src
self.dst = dst
self.pkt = pkt
self.rate = rate
self.dead = dead
self.seen = {}
self.running = False
self.route_time = None
self.listeners = []
self.sdt = SdtClient((sdt_host, sdt_port))
self.nodes = self.get_nodes()
def get_nodes(self) -> Dict[int, str]:
with self.core.context_connect():
if self.session is None:
self.session = self.get_session()
print("session: ", self.session)
try:
session = self.core.get_session(self.session)
node_map = {}
for node in session.nodes.values():
if node.type != NodeType.DEFAULT:
continue
node_map[node.id] = node.channel
if self.src_id is None:
_, ifaces, _ = self.core.get_node(self.session, node.id)
for iface in ifaces:
if self.src == iface.ip4:
self.src_id = node.id
break
except grpc.RpcError:
print(f"invalid session: {self.session}")
sys.exit(1)
if self.src_id is None:
print(f"could not find node with source address: {self.src}")
sys.exit(1)
print(
f"monitoring src_id ({self.src_id}) src({self.src}) dst({self.dst}) pkt({self.pkt})"
)
return node_map
def get_session(self) -> int:
sessions = self.core.get_sessions()
session = None
if sessions:
session = sessions[0]
if not session:
print("no current core sessions")
sys.exit(1)
return session.id
def start(self) -> None:
self.running = True
for node_id, node in self.nodes.items():
print("listening on node: ", node)
thread = Thread(target=self.listen, args=(node_id, node), daemon=True)
thread.start()
self.listeners.append(thread)
self.manage()
def manage(self) -> None:
self.route_time = time.monotonic()
while self.running:
route_enum, node, seen = self.queue.get()
if route_enum == RouteEnum.ADD:
self.seen[node] = seen
elif node in self.seen:
del self.seen[node]
if (time.monotonic() - self.route_time) >= self.rate:
self.manage_routes()
self.route_time = time.monotonic()
def route_sort(self, x: Tuple[str, int], y: Tuple[str, int]) -> int:
x_node = x[0]
y_node = y[0]
if x_node == self.src_id:
return 1
if y_node == self.src_id:
return -1
x_ttl, y_ttl = x[1], y[1]
return x_ttl - y_ttl
def manage_routes(self) -> None:
self.sdt.delete_links()
if not self.seen:
return
values = sorted(
self.seen.items(), key=cmp_to_key(self.route_sort), reverse=True
)
print("current route:")
for index, node_data in enumerate(values):
next_index = index + 1
if next_index == len(values):
break
next_node_id = values[next_index][0]
node_id, ttl = node_data
print(f"{node_id} -> {next_node_id}")
self.sdt.add_link(node_id, next_node_id)
def stop(self) -> None:
self.running = False
self.sdt.delete_links()
self.sdt.close()
for thread in self.listeners:
thread.join()
self.listeners.clear()
def listen(self, node_id, node) -> None:
cmd = f"tcpdump -lnvi any src host {self.src} and dst host {self.dst} and {self.pkt}"
node_cmd = f"vcmd -c {node} -- {cmd}"
p = subprocess.Popen(
node_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
)
current = time.monotonic()
try:
while not p.poll() and self.running:
ready, _, _ = select.select([p.stdout], [], [], 1)
if ready:
line = p.stdout.readline().strip().decode()
if line:
line = line.split("ttl", 1)[1]
ttl = int(line.split(",", 1)[0])
p.stdout.readline()
self.queue.put((RouteEnum.ADD, node_id, ttl))
current = time.monotonic()
else:
if (time.monotonic() - current) >= self.dead:
self.queue.put((RouteEnum.DEL, node_id, None))
except Exception as e:
print(f"listener error: {e}")
def main() -> None:
if not utils.which("tcpdump", required=False):
print("core-route-monitor requires tcpdump to be installed")
return
desc = "core route monitor leverages tcpdump to monitor traffic and find route using TTL"
parser = argparse.ArgumentParser(
description=desc, formatter_class=ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"--src", required=True, help="source address for route monitoring"
)
parser.add_argument(
"--dst", required=True, help="destination address for route monitoring"
)
parser.add_argument("--session", type=int, help="session to monitor route")
parser.add_argument(
"--pkt", default="icmp", choices=PACKET_CHOICES, help="packet type"
)
parser.add_argument(
"--rate", type=int, default=ROUTE_TIME, help="rate to update route, in seconds"
)
parser.add_argument(
"--dead",
type=int,
default=DEAD_TIME,
help="timeout to declare path dead, in seconds",
)
parser.add_argument("--sdt-host", default=SDT_HOST, help="sdt host address")
parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port")
args = parser.parse_args()
monitor = RouterMonitor(
args.session,
args.src,
args.dst,
args.pkt,
args.rate,
args.dead,
args.sdt_host,
args.sdt_port,
)
try:
monitor.start()
except KeyboardInterrupt:
monitor.stop()
print("ending route monitor")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,71 @@
import argparse
import re
from io import TextIOWrapper
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Helps transition older CORE services to work with newer versions"
)
parser.add_argument(
"-f",
"--file",
dest="file",
type=argparse.FileType("r"),
help="service file to update",
)
return parser.parse_args()
def update_service(service_file: TextIOWrapper) -> None:
update = []
for line in service_file.readlines():
# update service attributes
line = re.sub(r"^(\s+)_([a-z])", r"\1\2", line)
# rename dirs to directories
line = re.sub(r"^(\s+)dirs", r"\1directories", line)
# fix import states for service
line = re.sub(
r"^.+import.+CoreService.+$",
r"from core.services.coreservices import CoreService",
line,
)
# fix method signatures
line = re.sub(
r"def generateconfig\(cls, node, filename, services\)",
r"def generate_config(cls, node, filename)",
line,
)
line = re.sub(
r"def getvalidate\(cls, node, services\)",
r"def get_validate(cls, node)",
line,
)
line = re.sub(
r"def getstartup\(cls, node, services\)",
r"def get_startup(cls, node)",
line,
)
line = re.sub(
r"def getconfigfilenames\(cls, nodenum, services\)",
r"def get_configs(cls, node)",
line,
)
# remove unwanted lines
if re.search(r"addservice\(", line):
continue
if re.search(r"from.+\.ipaddr|import ipaddr", line):
continue
if re.search(r"from.+\.ipaddress|import ipaddress", line):
continue
# add modified line to make updated copy
update.append(line)
service_file.close()
with open(f"{service_file.name}.update", "w") as f:
f.writelines(update)
if __name__ == "__main__":
args = parse_args()
update_service(args.file)