daemon: refactoring to remove usage of os.path where possible and pathlib.Path instead

This commit is contained in:
Blake Harnden 2021-03-19 16:54:24 -07:00
parent d0a55dd471
commit 1c970bbe00
38 changed files with 520 additions and 606 deletions

View file

@ -271,11 +271,11 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
node_dir = None node_dir = None
config_services = [] config_services = []
if isinstance(node, CoreNodeBase): if isinstance(node, CoreNodeBase):
node_dir = node.nodedir node_dir = str(node.nodedir)
config_services = [x for x in node.config_services] config_services = [x for x in node.config_services]
channel = None channel = None
if isinstance(node, CoreNode): if isinstance(node, CoreNode):
channel = node.ctrlchnlname channel = str(node.ctrlchnlname)
emane_model = None emane_model = None
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.model.name emane_model = node.model.name

View file

@ -6,6 +6,7 @@ import tempfile
import threading import threading
import time import time
from concurrent import futures from concurrent import futures
from pathlib import Path
from typing import Iterable, Optional, Pattern, Type from typing import Iterable, Optional, Pattern, Type
import grpc import grpc
@ -221,8 +222,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
# clear previous state and setup for creation # clear previous state and setup for creation
session.clear() session.clear()
if not os.path.exists(session.session_dir): session.session_dir.mkdir(exist_ok=True)
os.mkdir(session.session_dir)
session.set_state(EventTypes.CONFIGURATION_STATE) session.set_state(EventTypes.CONFIGURATION_STATE)
# location # location
@ -366,12 +366,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
sessions = [] sessions = []
for session_id in self.coreemu.sessions: for session_id in self.coreemu.sessions:
session = self.coreemu.sessions[session_id] session = self.coreemu.sessions[session_id]
session_file = str(session.file_path) if session.file_path else None
session_summary = core_pb2.SessionSummary( session_summary = core_pb2.SessionSummary(
id=session_id, id=session_id,
state=session.state.value, state=session.state.value,
nodes=session.get_node_count(), nodes=session.get_node_count(),
file=session.file_name, file=session_file,
dir=session.session_dir, dir=str(session.session_dir),
) )
sessions.append(session_summary) sessions.append(session_summary)
return core_pb2.GetSessionsResponse(sessions=sessions) return core_pb2.GetSessionsResponse(sessions=sessions)
@ -423,14 +424,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("set session state: %s", request) logging.debug("set session state: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
try: try:
state = EventTypes(request.state) state = EventTypes(request.state)
session.set_state(state) session.set_state(state)
if state == EventTypes.INSTANTIATION_STATE: if state == EventTypes.INSTANTIATION_STATE:
if not os.path.exists(session.session_dir): session.session_dir.mkdir(exist_ok=True)
os.mkdir(session.session_dir)
session.instantiate() session.instantiate()
elif state == EventTypes.SHUTDOWN_STATE: elif state == EventTypes.SHUTDOWN_STATE:
session.shutdown() session.shutdown()
@ -438,11 +436,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
session.data_collect() session.data_collect()
elif state == EventTypes.DEFINITION_STATE: elif state == EventTypes.DEFINITION_STATE:
session.clear() session.clear()
result = True result = True
except KeyError: except KeyError:
result = False result = False
return core_pb2.SetSessionStateResponse(result=result) return core_pb2.SetSessionStateResponse(result=result)
def SetSessionUser( def SetSessionUser(
@ -573,12 +569,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
mobility_configs = grpcutils.get_mobility_configs(session) mobility_configs = grpcutils.get_mobility_configs(session)
service_configs = grpcutils.get_node_service_configs(session) service_configs = grpcutils.get_node_service_configs(session)
config_service_configs = grpcutils.get_node_config_service_configs(session) config_service_configs = grpcutils.get_node_config_service_configs(session)
session_file = str(session.file_path) if session.file_path else None
session_proto = core_pb2.Session( session_proto = core_pb2.Session(
id=session.id, id=session.id,
state=session.state.value, state=session.state.value,
nodes=nodes, nodes=nodes,
links=links, links=links,
dir=session.session_dir, dir=str(session.session_dir),
user=session.user, user=session.user,
default_services=default_services, default_services=default_services,
location=location, location=location,
@ -591,7 +588,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
mobility_configs=mobility_configs, mobility_configs=mobility_configs,
metadata=session.metadata, metadata=session.metadata,
file=session.file_name, file=session_file,
) )
return core_pb2.GetSessionResponse(session=session_proto) return core_pb2.GetSessionResponse(session=session_proto)
@ -1508,15 +1505,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("open xml: %s", request) logging.debug("open xml: %s", request)
session = self.coreemu.create_session() session = self.coreemu.create_session()
temp = tempfile.NamedTemporaryFile(delete=False) temp = tempfile.NamedTemporaryFile(delete=False)
temp.write(request.data.encode("utf-8")) temp.write(request.data.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name)
file_path = Path(request.file)
try: try:
session.open_xml(temp.name, request.start) session.open_xml(temp_path, request.start)
session.name = os.path.basename(request.file) session.name = file_path.name
session.file_name = request.file session.file_path = file_path
return core_pb2.OpenXmlResponse(session_id=session.id, result=True) return core_pb2.OpenXmlResponse(session_id=session.id, result=True)
except IOError: except IOError:
logging.exception("error opening session file") logging.exception("error opening session file")
@ -1733,12 +1730,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def ExecuteScript(self, request, context): def ExecuteScript(self, request, context):
existing_sessions = set(self.coreemu.sessions.keys()) existing_sessions = set(self.coreemu.sessions.keys())
file_path = Path(request.script)
thread = threading.Thread( thread = threading.Thread(
target=utils.execute_file, target=utils.execute_file,
args=( args=(file_path, {"coreemu": self.coreemu}),
request.script,
{"__file__": request.script, "coreemu": self.coreemu},
),
daemon=True, daemon=True,
) )
thread.start() thread.start()

View file

@ -3,7 +3,6 @@ socket server request handlers leveraged by core servers.
""" """
import logging import logging
import os
import shlex import shlex
import shutil import shutil
import socketserver import socketserver
@ -11,6 +10,7 @@ import sys
import threading import threading
import time import time
from itertools import repeat from itertools import repeat
from pathlib import Path
from queue import Empty, Queue from queue import Empty, Queue
from typing import Optional from typing import Optional
@ -167,39 +167,27 @@ class CoreHandler(socketserver.BaseRequestHandler):
date_list = [] date_list = []
thumb_list = [] thumb_list = []
num_sessions = 0 num_sessions = 0
with self._sessions_lock: with self._sessions_lock:
for _id in self.coreemu.sessions: for _id in self.coreemu.sessions:
session = self.coreemu.sessions[_id] session = self.coreemu.sessions[_id]
num_sessions += 1 num_sessions += 1
id_list.append(str(_id)) id_list.append(str(_id))
name = session.name name = session.name
if not name: if not name:
name = "" name = ""
name_list.append(name) name_list.append(name)
file_name = str(session.file_path) if session.file_path else ""
file_name = session.file_name file_list.append(str(file_name))
if not file_name:
file_name = ""
file_list.append(file_name)
node_count_list.append(str(session.get_node_count())) node_count_list.append(str(session.get_node_count()))
date_list.append(time.ctime(session.state_time)) date_list.append(time.ctime(session.state_time))
thumb = str(session.thumbnail) if session.thumbnail else ""
thumb = session.thumbnail
if not thumb:
thumb = ""
thumb_list.append(thumb) thumb_list.append(thumb)
session_ids = "|".join(id_list) session_ids = "|".join(id_list)
names = "|".join(name_list) names = "|".join(name_list)
files = "|".join(file_list) files = "|".join(file_list)
node_counts = "|".join(node_count_list) node_counts = "|".join(node_count_list)
dates = "|".join(date_list) dates = "|".join(date_list)
thumbs = "|".join(thumb_list) thumbs = "|".join(thumb_list)
if num_sessions > 0: if num_sessions > 0:
tlv_data = b"" tlv_data = b""
if len(session_ids) > 0: if len(session_ids) > 0:
@ -221,7 +209,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
message = coreapi.CoreSessionMessage.pack(flags, tlv_data) message = coreapi.CoreSessionMessage.pack(flags, tlv_data)
else: else:
message = None message = None
return message return message
def handle_broadcast_event(self, event_data): def handle_broadcast_event(self, event_data):
@ -931,22 +918,18 @@ class CoreHandler(socketserver.BaseRequestHandler):
if message.flags & MessageFlags.STRING.value: if message.flags & MessageFlags.STRING.value:
old_session_ids = set(self.coreemu.sessions.keys()) old_session_ids = set(self.coreemu.sessions.keys())
sys.argv = shlex.split(execute_server) sys.argv = shlex.split(execute_server)
file_name = sys.argv[0] file_path = Path(sys.argv[0])
if file_path.suffix == ".xml":
if os.path.splitext(file_name)[1].lower() == ".xml":
session = self.coreemu.create_session() session = self.coreemu.create_session()
try: try:
session.open_xml(file_name) session.open_xml(file_path)
except Exception: except Exception:
self.coreemu.delete_session(session.id) self.coreemu.delete_session(session.id)
raise raise
else: else:
thread = threading.Thread( thread = threading.Thread(
target=utils.execute_file, target=utils.execute_file,
args=( args=(file_path, {"coreemu": self.coreemu}),
file_name,
{"__file__": file_name, "coreemu": self.coreemu},
),
daemon=True, daemon=True,
) )
thread.start() thread.start()
@ -1465,10 +1448,12 @@ class CoreHandler(socketserver.BaseRequestHandler):
:return: reply messages :return: reply messages
""" """
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
node_num = message.get_tlv(FileTlvs.NODE.value) node_id = message.get_tlv(FileTlvs.NODE.value)
file_name = message.get_tlv(FileTlvs.NAME.value) file_name = message.get_tlv(FileTlvs.NAME.value)
file_type = message.get_tlv(FileTlvs.TYPE.value) file_type = message.get_tlv(FileTlvs.TYPE.value)
source_name = message.get_tlv(FileTlvs.SOURCE_NAME.value) src_path = message.get_tlv(FileTlvs.SOURCE_NAME.value)
if src_path:
src_path = Path(src_path)
data = message.get_tlv(FileTlvs.DATA.value) data = message.get_tlv(FileTlvs.DATA.value)
compressed_data = message.get_tlv(FileTlvs.COMPRESSED_DATA.value) compressed_data = message.get_tlv(FileTlvs.COMPRESSED_DATA.value)
@ -1478,7 +1463,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
) )
return () return ()
if source_name and data: if src_path and data:
logging.warning( logging.warning(
"ignoring invalid File message: source and data TLVs are both present" "ignoring invalid File message: source and data TLVs are both present"
) )
@ -1490,7 +1475,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
if file_type.startswith("service:"): if file_type.startswith("service:"):
_, service_name = file_type.split(":")[:2] _, service_name = file_type.split(":")[:2]
self.session.services.set_service_file( self.session.services.set_service_file(
node_num, service_name, file_name, data node_id, service_name, file_name, data
) )
return () return ()
elif file_type.startswith("hook:"): elif file_type.startswith("hook:"):
@ -1500,19 +1485,20 @@ class CoreHandler(socketserver.BaseRequestHandler):
return () return ()
state = int(state) state = int(state)
state = EventTypes(state) state = EventTypes(state)
self.session.add_hook(state, file_name, data, source_name) self.session.add_hook(state, file_name, data, src_path)
return () return ()
# writing a file to the host # writing a file to the host
if node_num is None: if node_id is None:
if source_name is not None: if src_path is not None:
shutil.copy2(source_name, file_name) shutil.copy2(src_path, file_name)
else: else:
with open(file_name, "w") as open_file: with file_name.open("w") as f:
open_file.write(data) f.write(data)
return () return ()
self.session.add_node_file(node_num, source_name, file_name, data) file_path = Path(file_name)
self.session.add_node_file(node_id, src_path, file_path, data)
else: else:
raise NotImplementedError raise NotImplementedError
@ -1567,26 +1553,32 @@ class CoreHandler(socketserver.BaseRequestHandler):
"dropping unhandled event message for node: %s", node.name "dropping unhandled event message for node: %s", node.name
) )
return () return ()
self.session.set_state(event_type)
if event_type == EventTypes.DEFINITION_STATE: if event_type == EventTypes.DEFINITION_STATE:
self.session.set_state(event_type)
# clear all session objects in order to receive new definitions # clear all session objects in order to receive new definitions
self.session.clear() self.session.clear()
elif event_type == EventTypes.CONFIGURATION_STATE:
self.session.set_state(event_type)
elif event_type == EventTypes.INSTANTIATION_STATE: elif event_type == EventTypes.INSTANTIATION_STATE:
self.session.set_state(event_type)
if len(self.handler_threads) > 1: if len(self.handler_threads) > 1:
# TODO: sync handler threads here before continuing # TODO: sync handler threads here before continuing
time.sleep(2.0) # XXX time.sleep(2.0) # XXX
# done receiving node/link configuration, ready to instantiate # done receiving node/link configuration, ready to instantiate
self.session.instantiate() self.session.instantiate()
# after booting nodes attempt to send emulation id for nodes waiting on status # after booting nodes attempt to send emulation id for nodes
# waiting on status
for _id in self.session.nodes: for _id in self.session.nodes:
self.send_node_emulation_id(_id) self.send_node_emulation_id(_id)
elif event_type == EventTypes.RUNTIME_STATE: elif event_type == EventTypes.RUNTIME_STATE:
self.session.set_state(event_type)
logging.warning("Unexpected event message: RUNTIME state received") logging.warning("Unexpected event message: RUNTIME state received")
elif event_type == EventTypes.DATACOLLECT_STATE: elif event_type == EventTypes.DATACOLLECT_STATE:
self.session.data_collect() self.session.data_collect()
elif event_type == EventTypes.SHUTDOWN_STATE: elif event_type == EventTypes.SHUTDOWN_STATE:
self.session.set_state(event_type)
logging.warning("Unexpected event message: SHUTDOWN state received") logging.warning("Unexpected event message: SHUTDOWN state received")
elif event_type in { elif event_type in {
EventTypes.START, EventTypes.START,
@ -1613,13 +1605,13 @@ class CoreHandler(socketserver.BaseRequestHandler):
name, name,
) )
elif event_type == EventTypes.FILE_OPEN: elif event_type == EventTypes.FILE_OPEN:
filename = event_data.name file_path = Path(event_data.name)
self.session.open_xml(filename, start=False) self.session.open_xml(file_path, start=False)
self.send_objects() self.send_objects()
return () return ()
elif event_type == EventTypes.FILE_SAVE: elif event_type == EventTypes.FILE_SAVE:
filename = event_data.name file_path = Path(event_data.name)
self.session.save_xml(filename) self.session.save_xml(file_path)
elif event_type == EventTypes.SCHEDULED: elif event_type == EventTypes.SCHEDULED:
etime = event_data.time etime = event_data.time
node_id = event_data.node node_id = event_data.node
@ -1733,20 +1725,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
session = self.session session = self.session
else: else:
session = self.coreemu.sessions.get(session_id) session = self.coreemu.sessions.get(session_id)
if session is None: if session is None:
logging.warning("session %s not found", session_id) logging.warning("session %s not found", session_id)
continue continue
if names is not None: if names is not None:
session.name = names[index] session.name = names[index]
if files is not None: if files is not None:
session.file_name = files[index] session.file_path = Path(files[index])
if thumb: if thumb:
thumb = Path(thumb)
session.set_thumbnail(thumb) session.set_thumbnail(thumb)
if user: if user:
session.set_user(user) session.set_user(user)
elif ( elif (

View file

@ -2,8 +2,8 @@ import abc
import enum import enum
import inspect import inspect
import logging import logging
import pathlib
import time import time
from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from mako import exceptions from mako import exceptions
@ -46,7 +46,7 @@ class ConfigService(abc.ABC):
""" """
self.node: CoreNode = node self.node: CoreNode = node
class_file = inspect.getfile(self.__class__) class_file = inspect.getfile(self.__class__)
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates: TemplateLookup = TemplateLookup(directories=templates_path) self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config: Dict[str, Configuration] = {} self.config: Dict[str, Configuration] = {}
self.custom_templates: Dict[str, str] = {} self.custom_templates: Dict[str, str] = {}
@ -176,9 +176,10 @@ class ConfigService(abc.ABC):
:raises CoreError: when there is a failure creating a directory :raises CoreError: when there is a failure creating a directory
""" """
for directory in self.directories: for directory in self.directories:
dir_path = Path(directory)
try: try:
self.node.privatedir(directory) self.node.privatedir(dir_path)
except (CoreCommandError, ValueError): except (CoreCommandError, CoreError):
raise CoreError( raise CoreError(
f"node({self.node.name}) service({self.name}) " f"node({self.node.name}) service({self.name}) "
f"failure to create service directory: {directory}" f"failure to create service directory: {directory}"
@ -220,7 +221,7 @@ class ConfigService(abc.ABC):
""" """
templates = {} templates = {}
for name in self.files: for name in self.files:
basename = pathlib.Path(name).name basename = Path(name).name
if name in self.custom_templates: if name in self.custom_templates:
template = self.custom_templates[name] template = self.custom_templates[name]
template = self.clean_text(template) template = self.clean_text(template)
@ -240,12 +241,12 @@ class ConfigService(abc.ABC):
""" """
data = self.data() data = self.data()
for name in self.files: for name in self.files:
basename = pathlib.Path(name).name file_path = Path(name)
if name in self.custom_templates: if name in self.custom_templates:
text = self.custom_templates[name] text = self.custom_templates[name]
rendered = self.render_text(text, data) rendered = self.render_text(text, data)
elif self.templates.has_template(basename): elif self.templates.has_template(file_path.name):
rendered = self.render_template(basename, data) rendered = self.render_template(file_path.name, data)
else: else:
text = self.get_text_template(name) text = self.get_text_template(name)
rendered = self.render_text(text, data) rendered = self.render_text(text, data)
@ -256,7 +257,7 @@ class ConfigService(abc.ABC):
name, name,
rendered, rendered,
) )
self.node.nodefile(name, rendered) self.node.nodefile(file_path, rendered)
def run_startup(self, wait: bool) -> None: def run_startup(self, wait: bool) -> None:
""" """

View file

@ -1,5 +1,6 @@
import logging import logging
import pathlib import pathlib
from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
from core import utils from core import utils
@ -55,10 +56,10 @@ class ConfigServiceManager:
except CoreError as e: except CoreError as e:
raise CoreError(f"config service({service.name}): {e}") raise CoreError(f"config service({service.name}): {e}")
# make service available # make service available
self.services[name] = service self.services[name] = service
def load(self, path: str) -> List[str]: def load(self, path: Path) -> List[str]:
""" """
Search path provided for configurable services and add them for being managed. Search path provided for configurable services and add them for being managed.
@ -71,7 +72,7 @@ class ConfigServiceManager:
service_errors = [] service_errors = []
for subdir in subdirs: for subdir in subdirs:
logging.debug("loading config services from: %s", subdir) logging.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService) services = utils.load_classes(subdir, ConfigService)
for service in services: for service in services:
try: try:
self.add(service) self.add(service)

View file

@ -1,3 +1,5 @@
COREDPY_VERSION = "@PACKAGE_VERSION@" from pathlib import Path
CORE_CONF_DIR = "@CORE_CONF_DIR@"
CORE_DATA_DIR = "@CORE_DATA_DIR@" COREDPY_VERSION: str = "@PACKAGE_VERSION@"
CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@")
CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@")

View file

@ -3,7 +3,7 @@ commeffect.py: EMANE CommEffect model for CORE
""" """
import logging import logging
import os from pathlib import Path
from typing import Dict, List from typing import Dict, List
from lxml import etree from lxml import etree
@ -48,8 +48,8 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
external_config: List[Configuration] = [] external_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml) shim_xml_path = emane_prefix / "share/emane/manifest" / cls.shim_xml
cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults) cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
@classmethod @classmethod

View file

@ -8,6 +8,7 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from core import utils from core import utils
@ -175,17 +176,15 @@ class EmaneManager(ModelManager):
if not path: if not path:
logging.info("emane is not installed") logging.info("emane is not installed")
return return
# get version # get version
emane_version = utils.cmd("emane --version") emane_version = utils.cmd("emane --version")
logging.info("using emane: %s", emane_version) logging.info("using emane: %s", emane_version)
# load default emane models # load default emane models
self.load_models(EMANE_MODELS) self.load_models(EMANE_MODELS)
# load custom models # load custom models
custom_models_path = self.session.options.get_config("emane_models_dir") custom_models_path = self.session.options.get_config("emane_models_dir")
if custom_models_path: if custom_models_path is not None:
custom_models_path = Path(custom_models_path)
emane_models = utils.load_classes(custom_models_path, EmaneModel) emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models) self.load_models(emane_models)
@ -246,6 +245,7 @@ class EmaneManager(ModelManager):
emane_prefix = self.session.options.get_config( emane_prefix = self.session.options.get_config(
"emane_prefix", default=DEFAULT_EMANE_PREFIX "emane_prefix", default=DEFAULT_EMANE_PREFIX
) )
emane_prefix = Path(emane_prefix)
emane_model.load(emane_prefix) emane_model.load(emane_prefix)
self.models[emane_model.name] = emane_model self.models[emane_model.name] = emane_model
@ -398,9 +398,9 @@ class EmaneManager(ModelManager):
return self.ifaces_to_nems.get(iface) return self.ifaces_to_nems.get(iface)
def write_nem(self, iface: CoreInterface, nem_id: int) -> None: def write_nem(self, iface: CoreInterface, nem_id: int) -> None:
path = os.path.join(self.session.session_dir, "emane_nems") path = self.session.session_dir / "emane_nems"
try: try:
with open(path, "a") as f: with path.open("a") as f:
f.write(f"{iface.node.name} {iface.name} {nem_id}\n") f.write(f"{iface.node.name} {iface.name} {nem_id}\n")
except IOError: except IOError:
logging.exception("error writing to emane nem file") logging.exception("error writing to emane nem file")
@ -590,18 +590,17 @@ class EmaneManager(ModelManager):
if eventservicenetidx >= 0 and eventgroup != otagroup: if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev) node.node_net_client.create_route(eventgroup, eventdev)
# start emane # start emane
log_file = os.path.join(node.nodedir, f"{node.name}-emane.log") log_file = node.nodedir / f"{node.name}-emane.log"
platform_xml = os.path.join(node.nodedir, f"{node.name}-platform.xml") platform_xml = node.nodedir / f"{node.name}-platform.xml"
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
node.cmd(args) node.cmd(args)
logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon running: %s", node.name, args)
else: else:
path = self.session.session_dir log_file = self.session.session_dir / f"{node.name}-emane.log"
log_file = os.path.join(path, f"{node.name}-emane.log") platform_xml = self.session.session_dir / f"{node.name}-platform.xml"
platform_xml = os.path.join(path, f"{node.name}-platform.xml") args = f"{emanecmd} -f {log_file} {platform_xml}"
emanecmd += f" -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.session_dir)
node.host_cmd(emanecmd, cwd=path) logging.info("node(%s) host emane daemon running: %s", node.name, args)
logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
def install_iface(self, iface: CoreInterface) -> None: def install_iface(self, iface: CoreInterface) -> None:
emane_net = iface.net emane_net = iface.net
@ -869,7 +868,8 @@ class EmaneGlobalModel:
emane_prefix = self.session.options.get_config( emane_prefix = self.session.options.get_config(
"emane_prefix", default=DEFAULT_EMANE_PREFIX "emane_prefix", default=DEFAULT_EMANE_PREFIX
) )
emulator_xml = os.path.join(emane_prefix, "share/emane/manifest/nemmanager.xml") emane_prefix = Path(emane_prefix)
emulator_xml = emane_prefix / "share/emane/manifest/nemmanager.xml"
emulator_defaults = { emulator_defaults = {
"eventservicedevice": DEFAULT_DEV, "eventservicedevice": DEFAULT_DEV,
"eventservicegroup": "224.1.2.8:45703", "eventservicegroup": "224.1.2.8:45703",

View file

@ -1,4 +1,5 @@
import logging import logging
from pathlib import Path
from typing import Dict, List from typing import Dict, List
from core.config import Configuration from core.config import Configuration
@ -71,9 +72,10 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str:
return config_default return config_default
def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]: def parse(manifest_path: Path, defaults: Dict[str, str]) -> List[Configuration]:
""" """
Parses a valid emane manifest file and converts the provided configuration values into ones used by core. Parses a valid emane manifest file and converts the provided configuration values
into ones used by core.
:param manifest_path: absolute manifest file path :param manifest_path: absolute manifest file path
:param defaults: used to override default values for configurations :param defaults: used to override default values for configurations
@ -85,7 +87,7 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
return [] return []
# load configuration file # load configuration file
manifest_file = manifest.Manifest(manifest_path) manifest_file = manifest.Manifest(str(manifest_path))
manifest_configurations = manifest_file.getAllConfiguration() manifest_configurations = manifest_file.getAllConfiguration()
configurations = [] configurations = []

View file

@ -2,7 +2,7 @@
Defines Emane Models used within CORE. Defines Emane Models used within CORE.
""" """
import logging import logging
import os from pathlib import Path
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
@ -53,7 +53,7 @@ class EmaneModel(WirelessModel):
config_ignore: Set[str] = set() config_ignore: Set[str] = set()
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
""" """
Called after being loaded within the EmaneManager. Provides configured emane_prefix for Called after being loaded within the EmaneManager. Provides configured emane_prefix for
parsing xml files. parsing xml files.
@ -63,11 +63,10 @@ class EmaneModel(WirelessModel):
""" """
manifest_path = "share/emane/manifest" manifest_path = "share/emane/manifest"
# load mac configuration # load mac configuration
mac_xml_path = os.path.join(emane_prefix, manifest_path, cls.mac_xml) mac_xml_path = emane_prefix / manifest_path / cls.mac_xml
cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults) cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults)
# load phy configuration # load phy configuration
phy_xml_path = os.path.join(emane_prefix, manifest_path, cls.phy_xml) phy_xml_path = emane_prefix / manifest_path / cls.phy_xml
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
@classmethod @classmethod

View file

@ -1,7 +1,7 @@
""" """
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
""" """
import os from pathlib import Path
from core.emane import emanemodel from core.emane import emanemodel
@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
mac_xml: str = "ieee80211abgmaclayer.xml" mac_xml: str = "ieee80211abgmaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
) )
super().load(emane_prefix) super().load(emane_prefix)

View file

@ -1,7 +1,7 @@
""" """
rfpipe.py: EMANE RF-PIPE model for CORE rfpipe.py: EMANE RF-PIPE model for CORE
""" """
import os from pathlib import Path
from core.emane import emanemodel from core.emane import emanemodel
@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
mac_xml: str = "rfpipemaclayer.xml" mac_xml: str = "rfpipemaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
) )
super().load(emane_prefix) super().load(emane_prefix)

View file

@ -3,7 +3,7 @@ tdma.py: EMANE TDMA model bindings for CORE
""" """
import logging import logging
import os from pathlib import Path
from typing import Set from typing import Set
from core import constants, utils from core import constants, utils
@ -22,27 +22,25 @@ class EmaneTdmaModel(emanemodel.EmaneModel):
# add custom schedule options and ignore it when writing emane xml # add custom schedule options and ignore it when writing emane xml
schedule_name: str = "schedule" schedule_name: str = "schedule"
default_schedule: str = os.path.join( default_schedule: Path = (
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml" constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml"
) )
config_ignore: Set[str] = {schedule_name} config_ignore: Set[str] = {schedule_name}
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix, emane_prefix
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml", / "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml"
) )
super().load(emane_prefix) super().load(emane_prefix)
cls.mac_config.insert( config_item = Configuration(
0, _id=cls.schedule_name,
Configuration( _type=ConfigDataTypes.STRING,
_id=cls.schedule_name, default=str(cls.default_schedule),
_type=ConfigDataTypes.STRING, label="TDMA schedule file (core)",
default=cls.default_schedule,
label="TDMA schedule file (core)",
),
) )
cls.mac_config.insert(0, config_item)
def post_startup(self) -> None: def post_startup(self) -> None:
""" """

View file

@ -3,6 +3,7 @@ import logging
import os import os
import signal import signal
import sys import sys
from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
import core.services import core.services
@ -60,10 +61,11 @@ class CoreEmu:
# config services # config services
self.service_manager: ConfigServiceManager = ConfigServiceManager() self.service_manager: ConfigServiceManager = ConfigServiceManager()
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) config_services_path = Path(configservices.__file__).resolve().parent
self.service_manager.load(config_services_path) self.service_manager.load(config_services_path)
custom_dir = self.config.get("custom_config_services_dir") custom_dir = self.config.get("custom_config_services_dir")
if custom_dir: if custom_dir is not None:
custom_dir = Path(custom_dir)
self.service_manager.load(custom_dir) self.service_manager.load(custom_dir)
# check executables exist on path # check executables exist on path
@ -91,13 +93,12 @@ class CoreEmu:
""" """
# load default services # load default services
self.service_errors = core.services.load() self.service_errors = core.services.load()
# load custom services # load custom services
service_paths = self.config.get("custom_services_dir") service_paths = self.config.get("custom_services_dir")
logging.debug("custom service paths: %s", service_paths) logging.debug("custom service paths: %s", service_paths)
if service_paths: if service_paths is not None:
for service_path in service_paths.split(","): for service_path in service_paths.split(","):
service_path = service_path.strip() service_path = Path(service_path.strip())
custom_service_errors = ServiceManager.add_services(service_path) custom_service_errors = ServiceManager.add_services(service_path)
self.service_errors.extend(custom_service_errors) self.service_errors.extend(custom_service_errors)

View file

@ -6,6 +6,7 @@ import logging
import os import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Tuple from typing import TYPE_CHECKING, Callable, Dict, Tuple
@ -79,23 +80,23 @@ class DistributedServer:
stdout, stderr = e.streams_for_display() stdout, stderr = e.streams_for_display()
raise CoreCommandError(e.result.exited, cmd, stdout, stderr) raise CoreCommandError(e.result.exited, cmd, stdout, stderr)
def remote_put(self, source: str, destination: str) -> None: def remote_put(self, src_path: Path, dst_path: Path) -> None:
""" """
Push file to remote server. Push file to remote server.
:param source: source file to push :param src_path: source file to push
:param destination: destination file location :param dst_path: destination file location
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
self.conn.put(source, destination) self.conn.put(str(src_path), str(dst_path))
def remote_put_temp(self, destination: str, data: str) -> None: def remote_put_temp(self, dst_path: Path, data: str) -> None:
""" """
Remote push file contents to a remote server, using a temp file as an Remote push file contents to a remote server, using a temp file as an
intermediate step. intermediate step.
:param destination: file destination for data :param dst_path: file destination for data
:param data: data to store in remote file :param data: data to store in remote file
:return: nothing :return: nothing
""" """
@ -103,7 +104,7 @@ class DistributedServer:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(data.encode("utf-8")) temp.write(data.encode("utf-8"))
temp.close() temp.close()
self.conn.put(temp.name, destination) self.conn.put(temp.name, str(dst_path))
os.unlink(temp.name) os.unlink(temp.name)
@ -170,13 +171,11 @@ class DistributedController:
tunnels = self.tunnels[key] tunnels = self.tunnels[key]
for tunnel in tunnels: for tunnel in tunnels:
tunnel.shutdown() tunnel.shutdown()
# remove all remote session directories # remove all remote session directories
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
cmd = f"rm -rf {self.session.session_dir}" cmd = f"rm -rf {self.session.session_dir}"
server.remote_cmd(cmd) server.remote_cmd(cmd)
# clear tunnels # clear tunnels
self.tunnels.clear() self.tunnels.clear()
@ -212,14 +211,12 @@ class DistributedController:
tunnel = self.tunnels.get(key) tunnel = self.tunnels.get(key)
if tunnel is not None: if tunnel is not None:
return tunnel return tunnel
# local to server # local to server
logging.info( logging.info(
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
) )
local_tap = GreTap(session=self.session, remoteip=host, key=key) local_tap = GreTap(session=self.session, remoteip=host, key=key)
local_tap.net_client.set_iface_master(node.brname, local_tap.localname) local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
# server to local # server to local
logging.info( logging.info(
"remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key
@ -228,7 +225,6 @@ class DistributedController:
session=self.session, remoteip=self.address, key=key, server=server session=self.session, remoteip=self.address, key=key, server=server
) )
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname) remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
# save tunnels for shutdown # save tunnels for shutdown
tunnel = (local_tap, remote_tap) tunnel = (local_tap, remote_tap)
self.tunnels[key] = tunnel self.tunnels[key] = tunnel

View file

@ -103,13 +103,13 @@ class Session:
self.id: int = _id self.id: int = _id
# define and create session directory when desired # define and create session directory when desired
self.session_dir: str = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") self.session_dir: Path = Path(tempfile.gettempdir()) / f"pycore.{self.id}"
if mkdir: if mkdir:
os.mkdir(self.session_dir) self.session_dir.mkdir()
self.name: Optional[str] = None self.name: Optional[str] = None
self.file_name: Optional[str] = None self.file_path: Optional[Path] = None
self.thumbnail: Optional[str] = None self.thumbnail: Optional[Path] = None
self.user: Optional[str] = None self.user: Optional[str] = None
self.event_loop: EventLoop = EventLoop() self.event_loop: EventLoop = EventLoop()
self.link_colors: Dict[int, str] = {} self.link_colors: Dict[int, str] = {}
@ -641,42 +641,39 @@ class Session:
logging.info("session(%s) checking if active: %s", self.id, result) logging.info("session(%s) checking if active: %s", self.id, result)
return result return result
def open_xml(self, file_name: str, start: bool = False) -> None: def open_xml(self, file_path: Path, start: bool = False) -> None:
""" """
Import a session from the EmulationScript XML format. Import a session from the EmulationScript XML format.
:param file_name: xml file to load session from :param file_path: xml file to load session from
:param start: instantiate session if true, false otherwise :param start: instantiate session if true, false otherwise
:return: nothing :return: nothing
""" """
logging.info("opening xml: %s", file_name) logging.info("opening xml: %s", file_path)
# clear out existing session # clear out existing session
self.clear() self.clear()
# set state and read xml # set state and read xml
state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE
self.set_state(state) self.set_state(state)
self.name = os.path.basename(file_name) self.name = file_path.name
self.file_name = file_name self.file_path = file_path
CoreXmlReader(self).read(file_name) CoreXmlReader(self).read(file_path)
# start session if needed # start session if needed
if start: if start:
self.set_state(EventTypes.INSTANTIATION_STATE) self.set_state(EventTypes.INSTANTIATION_STATE)
self.instantiate() self.instantiate()
def save_xml(self, file_name: str) -> None: def save_xml(self, file_path: Path) -> None:
""" """
Export a session to the EmulationScript XML format. Export a session to the EmulationScript XML format.
:param file_name: file name to write session xml to :param file_path: file name to write session xml to
:return: nothing :return: nothing
""" """
CoreXmlWriter(self).write(file_name) CoreXmlWriter(self).write(file_path)
def add_hook( def add_hook(
self, state: EventTypes, file_name: str, data: str, source_name: str = None self, state: EventTypes, file_name: str, data: str, src_name: str = None
) -> None: ) -> None:
""" """
Store a hook from a received file message. Store a hook from a received file message.
@ -684,11 +681,11 @@ class Session:
:param state: when to run hook :param state: when to run hook
:param file_name: file name for hook :param file_name: file name for hook
:param data: hook data :param data: hook data
:param source_name: source name :param src_name: source name
:return: nothing :return: nothing
""" """
logging.info( logging.info(
"setting state hook: %s - %s source(%s)", state, file_name, source_name "setting state hook: %s - %s source(%s)", state, file_name, src_name
) )
hook = file_name, data hook = file_name, data
state_hooks = self.hooks.setdefault(state, []) state_hooks = self.hooks.setdefault(state, [])
@ -700,22 +697,22 @@ class Session:
self.run_hook(hook) self.run_hook(hook)
def add_node_file( def add_node_file(
self, node_id: int, source_name: str, file_name: str, data: str self, node_id: int, src_path: Path, file_path: Path, data: str
) -> None: ) -> None:
""" """
Add a file to a node. Add a file to a node.
:param node_id: node to add file to :param node_id: node to add file to
:param source_name: source file name :param src_path: source file path
:param file_name: file name to add :param file_path: file path to add
:param data: file data :param data: file data
:return: nothing :return: nothing
""" """
node = self.get_node(node_id, CoreNodeBase) node = self.get_node(node_id, CoreNodeBase)
if source_name is not None: if src_path is not None:
node.addfile(source_name, file_name) node.addfile(src_path, file_path)
elif data is not None: elif data is not None:
node.nodefile(file_name, data) node.nodefile(file_path, data)
def clear(self) -> None: def clear(self) -> None:
""" """
@ -879,9 +876,9 @@ class Session:
:param state: state to write to file :param state: state to write to file
:return: nothing :return: nothing
""" """
state_file = os.path.join(self.session_dir, "state") state_file = self.session_dir / "state"
try: try:
with open(state_file, "w") as f: with state_file.open("w") as f:
f.write(f"{state.value} {state.name}\n") f.write(f"{state.value} {state.name}\n")
except IOError: except IOError:
logging.exception("error writing state file: %s", state.name) logging.exception("error writing state file: %s", state.name)
@ -907,12 +904,12 @@ class Session:
""" """
file_name, data = hook file_name, data = hook
logging.info("running hook %s", file_name) logging.info("running hook %s", file_name)
file_path = os.path.join(self.session_dir, file_name) file_path = self.session_dir / file_name
log_path = os.path.join(self.session_dir, f"{file_name}.log") log_path = self.session_dir / f"{file_name}.log"
try: try:
with open(file_path, "w") as f: with file_path.open("w") as f:
f.write(data) f.write(data)
with open(log_path, "w") as f: with log_path.open("w") as f:
args = ["/bin/sh", file_name] args = ["/bin/sh", file_name]
subprocess.check_call( subprocess.check_call(
args, args,
@ -983,10 +980,10 @@ class Session:
""" """
self.emane.poststartup() self.emane.poststartup()
# create session deployed xml # create session deployed xml
xml_file_name = os.path.join(self.session_dir, "session-deployed.xml")
xml_writer = corexml.CoreXmlWriter(self) xml_writer = corexml.CoreXmlWriter(self)
corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario)
xml_writer.write(xml_file_name) xml_file_path = self.session_dir / "session-deployed.xml"
xml_writer.write(xml_file_path)
def get_environment(self, state: bool = True) -> Dict[str, str]: def get_environment(self, state: bool = True) -> Dict[str, str]:
""" """
@ -1001,9 +998,9 @@ class Session:
env["CORE_PYTHON"] = sys.executable env["CORE_PYTHON"] = sys.executable
env["SESSION"] = str(self.id) env["SESSION"] = str(self.id)
env["SESSION_SHORT"] = self.short_session_id() env["SESSION_SHORT"] = self.short_session_id()
env["SESSION_DIR"] = self.session_dir env["SESSION_DIR"] = str(self.session_dir)
env["SESSION_NAME"] = str(self.name) env["SESSION_NAME"] = str(self.name)
env["SESSION_FILENAME"] = str(self.file_name) env["SESSION_FILENAME"] = str(self.file_path)
env["SESSION_USER"] = str(self.user) env["SESSION_USER"] = str(self.user)
if state: if state:
env["SESSION_STATE"] = str(self.state) env["SESSION_STATE"] = str(self.state)
@ -1011,8 +1008,8 @@ class Session:
# /etc/core/environment # /etc/core/environment
# /home/user/.core/environment # /home/user/.core/environment
# /tmp/pycore.<session id>/environment # /tmp/pycore.<session id>/environment
core_env_path = Path(constants.CORE_CONF_DIR) / "environment" core_env_path = constants.CORE_CONF_DIR / "environment"
session_env_path = Path(self.session_dir) / "environment" session_env_path = self.session_dir / "environment"
if self.user: if self.user:
user_home_path = Path(f"~{self.user}").expanduser() user_home_path = Path(f"~{self.user}").expanduser()
user_env1 = user_home_path / ".core" / "environment" user_env1 = user_home_path / ".core" / "environment"
@ -1028,20 +1025,20 @@ class Session:
logging.exception("error reading environment file: %s", path) logging.exception("error reading environment file: %s", path)
return env return env
def set_thumbnail(self, thumb_file: str) -> None: def set_thumbnail(self, thumb_file: Path) -> None:
""" """
Set the thumbnail filename. Move files from /tmp to session dir. Set the thumbnail filename. Move files from /tmp to session dir.
:param thumb_file: tumbnail file to set for session :param thumb_file: tumbnail file to set for session
:return: nothing :return: nothing
""" """
if not os.path.exists(thumb_file): if not thumb_file.is_file():
logging.error("thumbnail file to set does not exist: %s", thumb_file) logging.error("thumbnail file to set does not exist: %s", thumb_file)
self.thumbnail = None self.thumbnail = None
return return
destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file)) dst_path = self.session_dir / thumb_file.name
shutil.copy(thumb_file, destination_file) shutil.copy(thumb_file, dst_path)
self.thumbnail = destination_file self.thumbnail = dst_path
def set_user(self, user: str) -> None: def set_user(self, user: str) -> None:
""" """
@ -1054,7 +1051,7 @@ class Session:
if user: if user:
try: try:
uid = pwd.getpwnam(user).pw_uid uid = pwd.getpwnam(user).pw_uid
gid = os.stat(self.session_dir).st_gid gid = self.session_dir.stat().st_gid
os.chown(self.session_dir, uid, gid) os.chown(self.session_dir, uid, gid)
except IOError: except IOError:
logging.exception("failed to set permission on %s", self.session_dir) logging.exception("failed to set permission on %s", self.session_dir)
@ -1140,10 +1137,10 @@ class Session:
Write nodes to a 'nodes' file in the session dir. Write nodes to a 'nodes' file in the session dir.
The 'nodes' file lists: number, name, api-type, class-type The 'nodes' file lists: number, name, api-type, class-type
""" """
file_path = os.path.join(self.session_dir, "nodes") file_path = self.session_dir / "nodes"
try: try:
with self.nodes_lock: with self.nodes_lock:
with open(file_path, "w") as f: with file_path.open("w") as f:
for _id, node in self.nodes.items(): for _id, node in self.nodes.items():
f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n")
except IOError: except IOError:
@ -1268,15 +1265,13 @@ class Session:
# stop event loop # stop event loop
self.event_loop.stop() self.event_loop.stop()
# stop node services # stop mobility and node services
with self.nodes_lock: with self.nodes_lock:
funcs = [] funcs = []
for node_id in self.nodes: for node in self.nodes.values():
node = self.nodes[node_id] if isinstance(node, CoreNodeBase) and node.up:
if not isinstance(node, CoreNodeBase) or not node.up: args = (node,)
continue funcs.append((self.services.stop_services, args, {}))
args = (node,)
funcs.append((self.services.stop_services, args, {}))
utils.threadpool(funcs) utils.threadpool(funcs)
# shutdown emane # shutdown emane

View file

@ -1,6 +1,6 @@
import logging import logging
import os
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
@ -579,11 +579,12 @@ class ServiceConfigDialog(Dialog):
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def add_directory(self) -> None: def add_directory(self) -> None:
d = self.directory_entry.get() directory = self.directory_entry.get()
if os.path.isdir(d): directory = Path(directory)
if d not in self.temp_directories: if directory.is_dir():
self.dir_list.listbox.insert("end", d) if str(directory) not in self.temp_directories:
self.temp_directories.append(d) self.dir_list.listbox.insert("end", directory)
self.temp_directories.append(directory)
def remove_directory(self) -> None: def remove_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()

View file

@ -1,8 +1,8 @@
import logging import logging
import os
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from functools import partial from functools import partial
from pathlib import Path
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
@ -272,12 +272,12 @@ class Menubar(tk.Menu):
menu.add_command(label="About", command=self.click_about) menu.add_command(label="About", command=self.click_about)
self.add_cascade(label="Help", menu=menu) self.add_cascade(label="Help", menu=menu)
def open_recent_files(self, filename: str) -> None: def open_recent_files(self, file_path: Path) -> None:
if os.path.isfile(filename): if file_path.is_file():
logging.debug("Open recent file %s", filename) logging.debug("Open recent file %s", file_path)
self.open_xml_task(filename) self.open_xml_task(file_path)
else: else:
logging.warning("File does not exist %s", filename) logging.warning("File does not exist %s", file_path)
def update_recent_files(self) -> None: def update_recent_files(self) -> None:
self.recent_menu.delete(0, tk.END) self.recent_menu.delete(0, tk.END)
@ -286,7 +286,7 @@ class Menubar(tk.Menu):
label=i, command=partial(self.open_recent_files, i) label=i, command=partial(self.open_recent_files, i)
) )
def click_save(self, _event=None) -> None: def click_save(self, _event: tk.Event = None) -> None:
if self.core.session.file: if self.core.session.file:
self.core.save_xml() self.core.save_xml()
else: else:
@ -314,7 +314,7 @@ class Menubar(tk.Menu):
if file_path: if file_path:
self.open_xml_task(file_path) self.open_xml_task(file_path)
def open_xml_task(self, file_path: str) -> None: def open_xml_task(self, file_path: Path) -> None:
self.add_recent_file_to_gui_config(file_path) self.add_recent_file_to_gui_config(file_path)
self.prompt_save_running_session() self.prompt_save_running_session()
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,)) task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,))
@ -324,21 +324,14 @@ class Menubar(tk.Menu):
dialog = ExecutePythonDialog(self.app) dialog = ExecutePythonDialog(self.app)
dialog.show() dialog.show()
def add_recent_file_to_gui_config(self, file_path) -> None: def add_recent_file_to_gui_config(self, file_path: Path) -> None:
recent_files = self.app.guiconfig.recentfiles recent_files = self.app.guiconfig.recentfiles
num_files = len(recent_files) file_path = str(file_path)
if num_files == 0: if file_path in recent_files:
recent_files.insert(0, file_path) recent_files.remove(file_path)
elif 0 < num_files <= MAX_FILES: recent_files.insert(0, file_path)
if file_path in recent_files: if len(recent_files) > MAX_FILES:
recent_files.remove(file_path) recent_files.pop()
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config() self.app.save_config()
self.app.menubar.update_recent_files() self.app.menubar.update_recent_files()

View file

@ -419,7 +419,7 @@ class BasicRangeModel(WirelessModel):
self.wlan.link(a, b) self.wlan.link(a, b)
self.sendlinkmsg(a, b) self.sendlinkmsg(a, b)
except KeyError: except KeyError:
logging.exception("error getting interfaces during calclinkS") logging.exception("error getting interfaces during calclink")
@staticmethod @staticmethod
def calcdistance( def calcdistance(
@ -920,7 +920,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:param _id: object id :param _id: object id
""" """
super().__init__(session, _id) super().__init__(session, _id)
self.file: Optional[str] = None self.file: Optional[Path] = None
self.autostart: Optional[str] = None self.autostart: Optional[str] = None
self.nodemap: Dict[int, int] = {} self.nodemap: Dict[int, int] = {}
self.script_start: Optional[str] = None self.script_start: Optional[str] = None
@ -928,7 +928,7 @@ class Ns2ScriptedMobility(WayPointMobility):
self.script_stop: Optional[str] = None self.script_stop: Optional[str] = None
def update_config(self, config: Dict[str, str]) -> None: def update_config(self, config: Dict[str, str]) -> None:
self.file = config["file"] self.file = Path(config["file"])
logging.info( logging.info(
"ns-2 scripted mobility configured for WLAN %d using file: %s", "ns-2 scripted mobility configured for WLAN %d using file: %s",
self.id, self.id,
@ -953,15 +953,15 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
filename = self.findfile(self.file) file_path = self.findfile(self.file)
try: try:
f = open(filename, "r") f = file_path.open("r")
except IOError: except IOError:
logging.exception( logging.exception(
"ns-2 scripted mobility failed to load file: %s", self.file "ns-2 scripted mobility failed to load file: %s", self.file
) )
return return
logging.info("reading ns-2 script file: %s", filename) logging.info("reading ns-2 script file: %s", file_path)
ln = 0 ln = 0
ix = iy = iz = None ix = iy = iz = None
inodenum = None inodenum = None
@ -977,13 +977,13 @@ class Ns2ScriptedMobility(WayPointMobility):
# waypoints: # waypoints:
# $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" # $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0"
parts = line.split() parts = line.split()
time = float(parts[2]) line_time = float(parts[2])
nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")] nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")]
x = float(parts[5]) x = float(parts[5])
y = float(parts[6]) y = float(parts[6])
z = None z = None
speed = float(parts[7].strip('"')) speed = float(parts[7].strip('"'))
self.addwaypoint(time, self.map(nodenum), x, y, z, speed) self.addwaypoint(line_time, self.map(nodenum), x, y, z, speed)
elif line[:7] == "$node_(": elif line[:7] == "$node_(":
# initial position (time=0, speed=0): # initial position (time=0, speed=0):
# $node_(6) set X_ 780.0 # $node_(6) set X_ 780.0
@ -1011,31 +1011,31 @@ class Ns2ScriptedMobility(WayPointMobility):
if ix is not None and iy is not None: if ix is not None and iy is not None:
self.addinitial(self.map(inodenum), ix, iy, iz) self.addinitial(self.map(inodenum), ix, iy, iz)
def findfile(self, file_name: str) -> str: def findfile(self, file_path: Path) -> Path:
""" """
Locate a script file. If the specified file doesn't exist, look in the Locate a script file. If the specified file doesn't exist, look in the
same directory as the scenario file, or in gui directories. same directory as the scenario file, or in gui directories.
:param file_name: file name to find :param file_path: file name to find
:return: absolute path to the file :return: absolute path to the file
:raises CoreError: when file is not found :raises CoreError: when file is not found
""" """
file_path = Path(file_name).expanduser() file_path = file_path.expanduser()
if file_path.exists(): if file_path.exists():
return str(file_path) return file_path
if self.session.file_name: if self.session.file_path:
file_path = Path(self.session.file_name).parent / file_name session_file_path = self.session.file_path.parent / file_path
if file_path.exists(): if session_file_path.exists():
return str(file_path) return session_file_path
if self.session.user: if self.session.user:
user_path = Path(f"~{self.session.user}").expanduser() user_path = Path(f"~{self.session.user}").expanduser()
file_path = user_path / ".core" / "configs" / file_name configs_path = user_path / ".core" / "configs" / file_path
if file_path.exists(): if configs_path.exists():
return str(file_path) return configs_path
file_path = user_path / ".coregui" / "mobility" / file_name mobility_path = user_path / ".coregui" / "mobility" / file_path
if file_path.exists(): if mobility_path.exists():
return str(file_path) return mobility_path
raise CoreError(f"invalid file: {file_name}") raise CoreError(f"invalid file: {file_path}")
def parsemap(self, mapstr: str) -> None: def parsemap(self, mapstr: str) -> None:
""" """
@ -1047,7 +1047,6 @@ class Ns2ScriptedMobility(WayPointMobility):
self.nodemap = {} self.nodemap = {}
if mapstr.strip() == "": if mapstr.strip() == "":
return return
for pair in mapstr.split(","): for pair in mapstr.split(","):
parts = pair.split(":") parts = pair.split(":")
try: try:
@ -1152,6 +1151,7 @@ class Ns2ScriptedMobility(WayPointMobility):
filename = self.script_stop filename = self.script_stop
if filename is None or filename == "": if filename is None or filename == "":
return return
filename = Path(filename)
filename = self.findfile(filename) filename = self.findfile(filename)
args = f"{BASH} {filename} {typestr}" args = f"{BASH} {filename} {typestr}"
utils.cmd( utils.cmd(

View file

@ -3,9 +3,9 @@ Defines the base logic for nodes used within core.
""" """
import abc import abc
import logging import logging
import os
import shutil import shutil
import threading import threading
from pathlib import Path
from threading import RLock from threading import RLock
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
@ -30,6 +30,8 @@ if TYPE_CHECKING:
CoreServices = List[Union[CoreService, Type[CoreService]]] CoreServices = List[Union[CoreService, Type[CoreService]]]
ConfigServiceType = Type[ConfigService] ConfigServiceType = Type[ConfigService]
PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")]
class NodeBase(abc.ABC): class NodeBase(abc.ABC):
""" """
@ -97,7 +99,7 @@ class NodeBase(abc.ABC):
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -221,7 +223,7 @@ class CoreNodeBase(NodeBase):
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.config_services: Dict[str, "ConfigService"] = {} self.config_services: Dict[str, "ConfigService"] = {}
self.nodedir: Optional[str] = None self.nodedir: Optional[Path] = None
self.tmpnodedir: bool = False self.tmpnodedir: bool = False
@abc.abstractmethod @abc.abstractmethod
@ -233,11 +235,11 @@ class CoreNodeBase(NodeBase):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
@ -245,12 +247,12 @@ class CoreNodeBase(NodeBase):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: Path, file_path: Path) -> None:
""" """
Add a file. Add a file.
:param srcname: source file name :param src_path: source file path
:param filename: file name to add :param file_path: file name to add
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
@ -302,6 +304,21 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError raise NotImplementedError
def host_path(self, path: Path, is_dir: bool = False) -> Path:
"""
Return the name of a node"s file on the host filesystem.
:param path: path to translate to host path
:param is_dir: True if path is a directory path, False otherwise
:return: path to file
"""
if is_dir:
directory = str(path).strip("/").replace("/", ".")
return self.nodedir / directory
else:
directory = str(path.parent).strip("/").replace("/", ".")
return self.nodedir / directory / path.name
def add_config_service(self, service_class: "ConfigServiceType") -> None: def add_config_service(self, service_class: "ConfigServiceType") -> None:
""" """
Adds a configuration service to the node. Adds a configuration service to the node.
@ -346,7 +363,7 @@ class CoreNodeBase(NodeBase):
:return: nothing :return: nothing
""" """
if self.nodedir is None: if self.nodedir is None:
self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") self.nodedir = self.session.session_dir / f"{self.name}.conf"
self.host_cmd(f"mkdir -p {self.nodedir}") self.host_cmd(f"mkdir -p {self.nodedir}")
self.tmpnodedir = True self.tmpnodedir = True
else: else:
@ -458,7 +475,7 @@ class CoreNode(CoreNodeBase):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, nodedir: Path = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
) -> None: ) -> None:
""" """
@ -472,14 +489,12 @@ class CoreNode(CoreNodeBase):
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.nodedir: Optional[str] = nodedir self.nodedir: Optional[Path] = nodedir
self.ctrlchnlname: str = os.path.abspath( self.ctrlchnlname: Path = self.session.session_dir / self.name
os.path.join(self.session.session_dir, self.name)
)
self.client: Optional[VnodeClient] = None self.client: Optional[VnodeClient] = None
self.pid: Optional[int] = None self.pid: Optional[int] = None
self.lock: RLock = RLock() self.lock: RLock = RLock()
self._mounts: List[Tuple[str, str]] = [] self._mounts: List[Tuple[Path, Path]] = []
self.node_net_client: LinuxNetClient = self.create_node_net_client( self.node_net_client: LinuxNetClient = self.create_node_net_client(
self.session.use_ovs() self.session.use_ovs()
) )
@ -549,8 +564,8 @@ class CoreNode(CoreNodeBase):
self.up = True self.up = True
# create private directories # create private directories
self.privatedir("/var/run") for dir_path in PRIVATE_DIRS:
self.privatedir("/var/log") self.privatedir(dir_path)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -561,29 +576,24 @@ class CoreNode(CoreNodeBase):
# nothing to do if node is not up # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
try: try:
# unmount all targets (NOTE: non-persistent mount namespaces are # unmount all targets (NOTE: non-persistent mount namespaces are
# removed by the kernel when last referencing process is killed) # removed by the kernel when last referencing process is killed)
self._mounts = [] self._mounts = []
# shutdown all interfaces # shutdown all interfaces
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
# kill node process if present # kill node process if present
try: try:
self.host_cmd(f"kill -9 {self.pid}") self.host_cmd(f"kill -9 {self.pid}")
except CoreCommandError: except CoreCommandError:
logging.exception("error killing process") logging.exception("error killing process")
# remove node directory if present # remove node directory if present
try: try:
self.host_cmd(f"rm -rf {self.ctrlchnlname}") self.host_cmd(f"rm -rf {self.ctrlchnlname}")
except CoreCommandError: except CoreCommandError:
logging.exception("error removing node directory") logging.exception("error removing node directory")
# clear interface data, close client, and mark self and not up # clear interface data, close client, and mark self and not up
self.ifaces.clear() self.ifaces.clear()
self.client.close() self.client.close()
@ -636,35 +646,32 @@ class CoreNode(CoreNodeBase):
else: else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}" return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def privatedir(self, path: str) -> None: def privatedir(self, dir_path: Path) -> None:
""" """
Create a private directory. Create a private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
if path[0] != "/": if not str(dir_path).startswith("/"):
raise ValueError(f"path not fully qualified: {path}") raise CoreError(f"private directory path not fully qualified: {dir_path}")
hostpath = os.path.join( host_path = self.host_path(dir_path, is_dir=True)
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") self.host_cmd(f"mkdir -p {host_path}")
) self.mount(host_path, dir_path)
self.host_cmd(f"mkdir -p {hostpath}")
self.mount(hostpath, path)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
logging.debug("node(%s) mounting: %s at %s", self.name, source, target) self.cmd(f"mkdir -p {target_path}")
self.cmd(f"mkdir -p {target}") self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
self.cmd(f"{MOUNT} -n --bind {source} {target}") self._mounts.append((src_path, target_path))
self._mounts.append((source, target))
def next_iface_id(self) -> int: def next_iface_id(self) -> int:
""" """
@ -851,86 +858,66 @@ class CoreNode(CoreNodeBase):
self.ifup(iface_id) self.ifup(iface_id)
return self.get_iface(iface_id) return self.get_iface(iface_id)
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: Path, file_path: Path) -> None:
""" """
Add a file. Add a file.
:param srcname: source file name :param src_path: source file path
:param filename: file name to add :param file_path: file name to add
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.info("adding file from %s to %s", srcname, filename) logging.info("adding file from %s to %s", src_path, file_path)
directory = os.path.dirname(filename) directory = file_path.parent
if self.server is None: if self.server is None:
self.client.check_cmd(f"mkdir -p {directory}") self.client.check_cmd(f"mkdir -p {directory}")
self.client.check_cmd(f"mv {srcname} {filename}") self.client.check_cmd(f"mv {src_path} {file_path}")
self.client.check_cmd("sync") self.client.check_cmd("sync")
else: else:
self.host_cmd(f"mkdir -p {directory}") self.host_cmd(f"mkdir -p {directory}")
self.server.remote_put(srcname, filename) self.server.remote_put(src_path, file_path)
def hostfilename(self, filename: str) -> str: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
"""
Return the name of a node"s file on the host filesystem.
:param filename: host file name
:return: path to file
"""
dirname, basename = os.path.split(filename)
if not basename:
raise ValueError(f"no basename for filename: {filename}")
if dirname and dirname[0] == "/":
dirname = dirname[1:]
dirname = dirname.replace("/", ".")
dirname = os.path.join(self.nodedir, dirname)
return os.path.join(dirname, basename)
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
""" """
hostfilename = self.hostfilename(filename) host_path = self.host_path(file_path)
dirname, _basename = os.path.split(hostfilename) directory = host_path.parent
if self.server is None: if self.server is None:
if not os.path.isdir(dirname): if not directory.exists():
os.makedirs(dirname, mode=0o755) directory.mkdir(parents=True, mode=0o755)
with open(hostfilename, "w") as open_file: with host_path.open("w") as f:
open_file.write(contents) f.write(contents)
os.chmod(open_file.name, mode) host_path.chmod(mode)
else: else:
self.host_cmd(f"mkdir -m {0o755:o} -p {dirname}") self.host_cmd(f"mkdir -m {0o755:o} -p {directory}")
self.server.remote_put_temp(hostfilename, contents) self.server.remote_put_temp(host_path, contents)
self.host_cmd(f"chmod {mode:o} {hostfilename}") self.host_cmd(f"chmod {mode:o} {host_path}")
logging.debug( logging.debug("node(%s) added file: %s; mode: 0%o", self.name, host_path, mode)
"node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode
)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: def nodefilecopy(self, file_path: Path, src_path: Path, mode: int = None) -> None:
""" """
Copy a file to a node, following symlinks and preserving metadata. Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified. Change file mode if specified.
:param filename: file name to copy file to :param file_path: file name to copy file to
:param srcfilename: file to copy :param src_path: file to copy
:param mode: mode to copy to :param mode: mode to copy to
:return: nothing :return: nothing
""" """
hostfilename = self.hostfilename(filename) host_path = self.host_path(file_path)
if self.server is None: if self.server is None:
shutil.copy2(srcfilename, hostfilename) shutil.copy2(src_path, host_path)
else: else:
self.server.remote_put(srcfilename, hostfilename) self.server.remote_put(src_path, host_path)
if mode is not None: if mode is not None:
self.host_cmd(f"chmod {mode:o} {hostfilename}") self.host_cmd(f"chmod {mode:o} {host_path}")
logging.info( logging.info("node(%s) copied file: %s; mode: %s", self.name, host_path, mode)
"node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode
)
class CoreNetworkBase(NodeBase): class CoreNetworkBase(NodeBase):

View file

@ -3,6 +3,7 @@ client.py: implementation of the VnodeClient class for issuing commands
over a control channel to the vnoded process running in a network namespace. over a control channel to the vnoded process running in a network namespace.
The control channel can be accessed via calls using the vcmd shell. The control channel can be accessed via calls using the vcmd shell.
""" """
from pathlib import Path
from core import utils from core import utils
from core.executables import BASH, VCMD from core.executables import BASH, VCMD
@ -13,7 +14,7 @@ class VnodeClient:
Provides client functionality for interacting with a virtual node. Provides client functionality for interacting with a virtual node.
""" """
def __init__(self, name: str, ctrlchnlname: str) -> None: def __init__(self, name: str, ctrlchnlname: Path) -> None:
""" """
Create a VnodeClient instance. Create a VnodeClient instance.
@ -21,7 +22,7 @@ class VnodeClient:
:param ctrlchnlname: control channel name :param ctrlchnlname: control channel name
""" """
self.name: str = name self.name: str = name
self.ctrlchnlname: str = ctrlchnlname self.ctrlchnlname: Path = ctrlchnlname
def _verify_connection(self) -> None: def _verify_connection(self) -> None:
""" """

View file

@ -1,6 +1,6 @@
import json import json
import logging import logging
import os 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, Callable, Dict, Optional
@ -63,8 +63,8 @@ class DockerClient:
logging.debug("node(%s) pid: %s", self.name, self.pid) logging.debug("node(%s) pid: %s", self.name, self.pid)
return output return output
def copy_file(self, source: str, destination: str) -> str: def copy_file(self, src_path: Path, dst_path: Path) -> str:
args = f"docker cp {source} {self.name}:{destination}" args = f"docker cp {src_path} {self.name}:{dst_path}"
return self.run(args) return self.run(args)
@ -162,77 +162,73 @@ class DockerNode(CoreNode):
""" """
return f"docker exec -it {self.name} bash" return f"docker exec -it {self.name} bash"
def privatedir(self, path: str) -> None: def privatedir(self, dir_path: str) -> None:
""" """
Create a private directory. Create a private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
logging.debug("creating node dir: %s", path) logging.debug("creating node dir: %s", dir_path)
args = f"mkdir -p {path}" args = f"mkdir -p {dir_path}"
self.cmd(args) self.cmd(args)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: str, target_path: str) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.debug("mounting source(%s) target(%s)", source, target) logging.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported") raise Exception("not supported")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
""" """
logging.debug("nodefile filename(%s) mode(%s)", filename, mode) logging.debug("nodefile filename(%s) mode(%s)", file_path, mode)
directory = os.path.dirname(filename)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name)
if directory: directory = file_path.name
if str(directory) != ".":
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.name, temp.name) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp.name, filename) self.client.copy_file(temp_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") 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.name}") self.host_cmd(f"rm -f {temp_path}")
os.unlink(temp.name) temp_path.unlink()
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) logging.debug("node(%s) added file: %s; mode: 0%o", self.name, file_path, mode)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: def nodefilecopy(self, file_path: Path, src_path: Path, mode: int = None) -> None:
""" """
Copy a file to a node, following symlinks and preserving metadata. Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified. Change file mode if specified.
:param filename: file name to copy file to :param file_path: file name to copy file to
:param srcfilename: file to copy :param src_path: file to copy
:param mode: mode to copy to :param mode: mode to copy to
:return: nothing :return: nothing
""" """
logging.info( logging.info(
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode "node file copy file(%s) source(%s) mode(%s)", file_path, src_path, mode
) )
directory = os.path.dirname(filename) self.cmd(f"mkdir -p {file_path.parent}")
self.cmd(f"mkdir -p {directory}") if self.server:
if self.server is None:
source = srcfilename
else:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
source = temp.name temp_path = Path(temp.name)
self.server.remote_put(source, temp.name) src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.client.copy_file(source, filename) self.client.copy_file(src_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") self.cmd(f"chmod {mode:o} {file_path}")

View file

@ -4,6 +4,7 @@ virtual ethernet classes that implement the interfaces available under Linux.
import logging import logging
import time import time
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
import netaddr import netaddr
@ -79,7 +80,7 @@ class CoreInterface:
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:

View file

@ -1,7 +1,7 @@
import json import json
import logging import logging
import os
import time import time
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, Callable, Dict, Optional
@ -57,11 +57,10 @@ class LxdClient:
args = self.create_cmd(cmd) args = self.create_cmd(cmd)
return utils.cmd(args, wait=wait, shell=shell) return utils.cmd(args, wait=wait, shell=shell)
def copy_file(self, source: str, destination: str) -> None: def copy_file(self, src_path: Path, dst_path: Path) -> None:
if destination[0] != "/": if not str(dst_path).startswith("/"):
destination = os.path.join("/root/", destination) dst_path = Path("/root/") / dst_path
args = f"lxc file push {src_path} {self.name}/{dst_path}"
args = f"lxc file push {source} {self.name}/{destination}"
self.run(args) self.run(args)
@ -139,81 +138,76 @@ class LxcNode(CoreNode):
""" """
return f"lxc exec {self.name} -- {sh}" return f"lxc exec {self.name} -- {sh}"
def privatedir(self, path: str) -> None: def privatedir(self, dir_path: Path) -> None:
""" """
Create a private directory. Create a private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
logging.info("creating node dir: %s", path) logging.info("creating node dir: %s", dir_path)
args = f"mkdir -p {path}" args = f"mkdir -p {dir_path}"
self.cmd(args) self.cmd(args)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.debug("mounting source(%s) target(%s)", source, target) logging.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported") raise Exception("not supported")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
""" """
logging.debug("nodefile filename(%s) mode(%s)", filename, mode) logging.debug("nodefile filename(%s) mode(%s)", file_path, mode)
directory = os.path.dirname(filename)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name)
if directory: directory = file_path.parent
if str(directory) != ".":
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.name, temp.name) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp.name, filename) self.client.copy_file(temp_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") 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.name}") self.host_cmd(f"rm -f {temp_path}")
os.unlink(temp.name) temp_path.unlink()
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) logging.debug("node(%s) added file: %s; mode: 0%o", self.name, file_path, mode)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: def nodefilecopy(self, file_path: Path, src_path: Path, mode: int = None) -> None:
""" """
Copy a file to a node, following symlinks and preserving metadata. Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified. Change file mode if specified.
:param filename: file name to copy file to :param file_path: file name to copy file to
:param srcfilename: file to copy :param src_path: file to copy
:param mode: mode to copy to :param mode: mode to copy to
:return: nothing :return: nothing
""" """
logging.info( logging.info(
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode "node file copy file(%s) source(%s) mode(%s)", file_path, src_path, mode
) )
directory = os.path.dirname(filename) self.cmd(f"mkdir -p {file_path.parent}")
self.cmd(f"mkdir -p {directory}") if self.server:
if self.server is None:
source = srcfilename
else:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
source = temp.name temp_path = Path(temp.name)
self.server.remote_put(source, temp.name) src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.client.copy_file(source, filename) self.client.copy_file(src_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") self.cmd(f"chmod {mode:o} {file_path}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None: def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
super().add_iface(iface, iface_id) super().add_iface(iface, iface_id)

View file

@ -6,6 +6,7 @@ import logging
import math import math
import threading import threading
import time import time
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type
import netaddr import netaddr
@ -292,7 +293,7 @@ class CoreNetwork(CoreNetworkBase):
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -333,9 +334,7 @@ class CoreNetwork(CoreNetworkBase):
""" """
if not self.up: if not self.up:
return return
ebq.stopupdateloop(self) ebq.stopupdateloop(self)
try: try:
self.net_client.delete_bridge(self.brname) self.net_client.delete_bridge(self.brname)
if self.has_ebtables_chain: if self.has_ebtables_chain:
@ -346,11 +345,9 @@ class CoreNetwork(CoreNetworkBase):
ebtablescmds(self.host_cmd, cmds) ebtablescmds(self.host_cmd, cmds)
except CoreCommandError: except CoreCommandError:
logging.exception("error during shutdown") logging.exception("error during shutdown")
# removes veth pairs used for bridge-to-bridge connections # removes veth pairs used for bridge-to-bridge connections
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
self.ifaces.clear() self.ifaces.clear()
self._linked.clear() self._linked.clear()
del self.session del self.session
@ -389,10 +386,8 @@ class CoreNetwork(CoreNetworkBase):
# check if the network interfaces are attached to this network # check if the network interfaces are attached to this network
if self.ifaces[iface1.net_id] != iface1: if self.ifaces[iface1.net_id] != iface1:
raise ValueError(f"inconsistency for interface {iface1.name}") raise ValueError(f"inconsistency for interface {iface1.name}")
if self.ifaces[iface2.net_id] != iface2: if self.ifaces[iface2.net_id] != iface2:
raise ValueError(f"inconsistency for interface {iface2.name}") raise ValueError(f"inconsistency for interface {iface2.name}")
try: try:
linked = self._linked[iface1][iface2] linked = self._linked[iface1][iface2]
except KeyError: except KeyError:
@ -403,7 +398,6 @@ class CoreNetwork(CoreNetworkBase):
else: else:
raise Exception(f"unknown policy: {self.policy.value}") raise Exception(f"unknown policy: {self.policy.value}")
self._linked[iface1][iface2] = linked self._linked[iface1][iface2] = linked
return linked return linked
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:

View file

@ -3,9 +3,9 @@ PhysicalNode class for including real systems in the emulated network.
""" """
import logging import logging
import os
import threading import threading
from typing import IO, TYPE_CHECKING, List, Optional, Tuple from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple
from core.emulator.data import InterfaceData, LinkOptions from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
@ -26,15 +26,15 @@ class PhysicalNode(CoreNodeBase):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, nodedir: Path = None,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
if not self.server: if not self.server:
raise CoreError("physical nodes must be assigned to a remote server") raise CoreError("physical nodes must be assigned to a remote server")
self.nodedir: Optional[str] = nodedir self.nodedir: Optional[Path] = nodedir
self.lock: threading.RLock = threading.RLock() self.lock: threading.RLock = threading.RLock()
self._mounts: List[Tuple[str, str]] = [] self._mounts: List[Tuple[Path, Path]] = []
def startup(self) -> None: def startup(self) -> None:
with self.lock: with self.lock:
@ -44,15 +44,12 @@ class PhysicalNode(CoreNodeBase):
def shutdown(self) -> None: def shutdown(self) -> None:
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
while self._mounts: while self._mounts:
_source, target = self._mounts.pop(-1) _, target_path = self._mounts.pop(-1)
self.umount(target) self.umount(target_path)
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
self.rmnodedir() self.rmnodedir()
def path_exists(self, path: str) -> bool: def path_exists(self, path: str) -> bool:
@ -186,55 +183,40 @@ class PhysicalNode(CoreNodeBase):
self.adopt_iface(iface, iface_id, iface_data.mac, ips) self.adopt_iface(iface, iface_id, iface_data.mac, ips)
return iface return iface
def privatedir(self, path: str) -> None: def privatedir(self, dir_path: Path) -> None:
if path[0] != "/": if not str(dir_path).startswith("/"):
raise ValueError(f"path not fully qualified: {path}") raise CoreError(f"private directory path not fully qualified: {dir_path}")
hostpath = os.path.join( host_path = self.host_path(dir_path, is_dir=True)
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") self.host_cmd(f"mkdir -p {host_path}")
) self.mount(host_path, dir_path)
os.mkdir(hostpath)
self.mount(hostpath, path)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
logging.info("mounting %s at %s", source, target) self.cmd(f"mkdir -p {target_path}")
os.makedirs(target) self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.nodedir)
self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir) self._mounts.append((src_path, target_path))
self._mounts.append((source, target))
def umount(self, target: str) -> None: def umount(self, target_path: Path) -> None:
logging.info("unmounting '%s'", target) logging.info("unmounting '%s'", target_path)
try: try:
self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir) self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.nodedir)
except CoreCommandError: except CoreCommandError:
logging.exception("unmounting failed for %s", target) logging.exception("unmounting failed for %s", target_path)
def opennodefile(self, filename: str, mode: str = "w") -> IO: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
dirname, basename = os.path.split(filename) host_path = self.host_path(file_path)
if not basename: directory = host_path.parent
raise ValueError("no basename for filename: " + filename) if not directory.is_dir():
directory.mkdir(parents=True, mode=0o755)
if dirname and dirname[0] == "/": with host_path.open("w") as f:
dirname = dirname[1:]
dirname = dirname.replace("/", ".")
dirname = os.path.join(self.nodedir, dirname)
if not os.path.isdir(dirname):
os.makedirs(dirname, mode=0o755)
hostfilename = os.path.join(dirname, basename)
return open(hostfilename, mode)
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None:
with self.opennodefile(filename, "w") as f:
f.write(contents) f.write(contents)
os.chmod(f.name, mode) host_path.chmod(mode)
logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode) logging.info("created nodefile: '%s'; mode: 0%o", host_path, mode)
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
return self.host_cmd(args, wait=wait) return self.host_cmd(args, wait=wait)
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: str, file_path: str) -> None:
raise CoreError("physical node does not support addfile") raise CoreError("physical node does not support addfile")
@ -464,10 +446,10 @@ class Rj45Node(CoreNodeBase):
def termcmdstring(self, sh: str) -> str: def termcmdstring(self, sh: str) -> str:
raise CoreError("rj45 does not support terminal commands") raise CoreError("rj45 does not support terminal commands")
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: str, file_path: str) -> None:
raise CoreError("rj45 does not support addfile") raise CoreError("rj45 does not support addfile")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def nodefile(self, file_path: str, contents: str, mode: int = 0o644) -> None:
raise CoreError("rj45 does not support nodefile") raise CoreError("rj45 does not support nodefile")
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:

View file

@ -4,11 +4,11 @@ Services
Services available to nodes can be put in this directory. Everything listed in Services available to nodes can be put in this directory. Everything listed in
__all__ is automatically loaded by the main core module. __all__ is automatically loaded by the main core module.
""" """
import os from pathlib import Path
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
_PATH = os.path.abspath(os.path.dirname(__file__)) _PATH: Path = Path(__file__).resolve().parent
def load(): def load():

View file

@ -10,6 +10,7 @@ services.
import enum import enum
import logging import logging
import time import time
from pathlib import Path
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Dict, Dict,
@ -264,7 +265,7 @@ class ServiceManager:
return service return service
@classmethod @classmethod
def add_services(cls, path: str) -> List[str]: def add_services(cls, path: Path) -> List[str]:
""" """
Method for retrieving all CoreServices from a given path. Method for retrieving all CoreServices from a given path.
@ -276,7 +277,6 @@ class ServiceManager:
for service in services: for service in services:
if not service.name: if not service.name:
continue continue
try: try:
cls.add(service) cls.add(service)
except (CoreError, ValueError) as e: except (CoreError, ValueError) as e:
@ -488,9 +488,10 @@ class CoreServices:
# create service directories # create service directories
for directory in service.dirs: for directory in service.dirs:
dir_path = Path(directory)
try: try:
node.privatedir(directory) node.privatedir(dir_path)
except (CoreCommandError, ValueError) as e: except (CoreCommandError, CoreError) as e:
logging.warning( logging.warning(
"error mounting private dir '%s' for service '%s': %s", "error mounting private dir '%s' for service '%s': %s",
directory, directory,
@ -534,14 +535,14 @@ class CoreServices:
"node(%s) service(%s) failed validation" % (node.name, service.name) "node(%s) service(%s) failed validation" % (node.name, service.name)
) )
def copy_service_file(self, node: CoreNode, filename: str, cfg: str) -> bool: def copy_service_file(self, node: CoreNode, file_path: Path, cfg: str) -> bool:
""" """
Given a configured service filename and config, determine if the Given a configured service filename and config, determine if the
config references an existing file that should be copied. config references an existing file that should be copied.
Returns True for local files, False for generated. Returns True for local files, False for generated.
:param node: node to copy service for :param node: node to copy service for
:param filename: file name for a configured service :param file_path: file name for a configured service
:param cfg: configuration string :param cfg: configuration string
:return: True if successful, False otherwise :return: True if successful, False otherwise
""" """
@ -550,7 +551,7 @@ class CoreServices:
src = src.split("\n")[0] src = src.split("\n")[0]
src = utils.expand_corepath(src, node.session, node) src = utils.expand_corepath(src, node.session, node)
# TODO: glob here # TODO: glob here
node.nodefilecopy(filename, src, mode=0o644) node.nodefilecopy(file_path, src, mode=0o644)
return True return True
return False return False
@ -729,8 +730,8 @@ class CoreServices:
config_files = service.configs config_files = service.configs
if not service.custom: if not service.custom:
config_files = service.get_configs(node) config_files = service.get_configs(node)
for file_name in config_files: for file_name in config_files:
file_path = Path(file_name)
logging.debug( logging.debug(
"generating service config custom(%s): %s", service.custom, file_name "generating service config custom(%s): %s", service.custom, file_name
) )
@ -738,18 +739,16 @@ class CoreServices:
cfg = service.config_data.get(file_name) cfg = service.config_data.get(file_name)
if cfg is None: if cfg is None:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
# cfg may have a file:/// url for copying from a file # cfg may have a file:/// url for copying from a file
try: try:
if self.copy_service_file(node, file_name, cfg): if self.copy_service_file(node, file_path, cfg):
continue continue
except IOError: except IOError:
logging.exception("error copying service file: %s", file_name) logging.exception("error copying service file: %s", file_name)
continue continue
else: else:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
node.nodefile(file_path, cfg)
node.nodefile(file_name, cfg)
def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None: def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None:
""" """
@ -762,17 +761,15 @@ class CoreServices:
config_files = service.configs config_files = service.configs
if not service.custom: if not service.custom:
config_files = service.get_configs(node) config_files = service.get_configs(node)
for file_name in config_files: for file_name in config_files:
file_path = Path(file_name)
if file_name[:7] == "file:///": if file_name[:7] == "file:///":
# TODO: implement this # TODO: implement this
raise NotImplementedError raise NotImplementedError
cfg = service.config_data.get(file_name) cfg = service.config_data.get(file_name)
if cfg is None: if cfg is None:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
node.nodefile(file_path, cfg)
node.nodefile(file_name, cfg)
class CoreService: class CoreService:

View file

@ -46,11 +46,10 @@ IFACE_CONFIG_FACTOR: int = 1000
def execute_file( def execute_file(
path: str, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None path: Path, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None
) -> None: ) -> None:
""" """
Provides an alternative way to run execfile to be compatible for Provides a way to execute a file.
both python2/3.
:param path: path of file to execute :param path: path of file to execute
:param exec_globals: globals values to pass to execution :param exec_globals: globals values to pass to execution
@ -59,10 +58,10 @@ def execute_file(
""" """
if exec_globals is None: if exec_globals is None:
exec_globals = {} exec_globals = {}
exec_globals.update({"__file__": path, "__name__": "__main__"}) exec_globals.update({"__file__": str(path), "__name__": "__main__"})
with open(path, "rb") as f: with path.open("rb") as f:
data = compile(f.read(), path, "exec") data = compile(f.read(), path, "exec")
exec(data, exec_globals, exec_locals) exec(data, exec_globals, exec_locals)
def hashkey(value: Union[str, int]) -> int: def hashkey(value: Union[str, int]) -> int:
@ -92,24 +91,19 @@ def _detach_init() -> None:
os.setsid() os.setsid()
def _valid_module(path: str, file_name: str) -> bool: def _valid_module(path: Path) -> bool:
""" """
Check if file is a valid python module. Check if file is a valid python module.
:param path: path to file :param path: path to file
:param file_name: file name to check
:return: True if a valid python module file, False otherwise :return: True if a valid python module file, False otherwise
""" """
file_path = os.path.join(path, file_name) if not path.is_file():
if not os.path.isfile(file_path):
return False return False
if path.name.startswith("_"):
if file_name.startswith("_"):
return False return False
if not path.suffix == ".py":
if not file_name.endswith(".py"):
return False return False
return True return True
@ -124,13 +118,10 @@ def _is_class(module: Any, member: Type, clazz: Type) -> bool:
""" """
if not inspect.isclass(member): if not inspect.isclass(member):
return False return False
if not issubclass(member, clazz): if not issubclass(member, clazz):
return False return False
if member.__module__ != module.__name__: if member.__module__ != module.__name__:
return False return False
return True return True
@ -196,7 +187,7 @@ def mute_detach(args: str, **kwargs: Dict[str, Any]) -> int:
def cmd( def cmd(
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -282,7 +273,7 @@ def file_demunge(pathname: str, header: str) -> None:
def expand_corepath( def expand_corepath(
pathname: str, session: "Session" = None, node: "CoreNode" = None pathname: str, session: "Session" = None, node: "CoreNode" = None
) -> str: ) -> Path:
""" """
Expand a file path given session information. Expand a file path given session information.
@ -294,14 +285,12 @@ def expand_corepath(
if session is not None: if session is not None:
pathname = pathname.replace("~", f"/home/{session.user}") pathname = pathname.replace("~", f"/home/{session.user}")
pathname = pathname.replace("%SESSION%", str(session.id)) pathname = pathname.replace("%SESSION%", str(session.id))
pathname = pathname.replace("%SESSION_DIR%", session.session_dir) pathname = pathname.replace("%SESSION_DIR%", str(session.session_dir))
pathname = pathname.replace("%SESSION_USER%", session.user) pathname = pathname.replace("%SESSION_USER%", session.user)
if node is not None: if node is not None:
pathname = pathname.replace("%NODE%", str(node.id)) pathname = pathname.replace("%NODE%", str(node.id))
pathname = pathname.replace("%NODENAME%", node.name) pathname = pathname.replace("%NODENAME%", node.name)
return Path(pathname)
return pathname
def sysctl_devname(devname: str) -> Optional[str]: def sysctl_devname(devname: str) -> Optional[str]:
@ -337,7 +326,7 @@ def load_config(file_path: Path, d: Dict[str, str]) -> None:
logging.exception("error reading file to dict: %s", file_path) logging.exception("error reading file to dict: %s", file_path)
def load_classes(path: str, clazz: Generic[T]) -> T: def load_classes(path: Path, clazz: Generic[T]) -> T:
""" """
Dynamically load classes for use within CORE. Dynamically load classes for use within CORE.
@ -347,24 +336,19 @@ def load_classes(path: str, clazz: Generic[T]) -> T:
""" """
# validate path exists # validate path exists
logging.debug("attempting to load modules from path: %s", path) logging.debug("attempting to load modules from path: %s", path)
if not os.path.isdir(path): if not path.is_dir():
logging.warning("invalid custom module directory specified" ": %s", path) logging.warning("invalid custom module directory specified" ": %s", path)
# check if path is in sys.path # check if path is in sys.path
parent_path = os.path.dirname(path) parent = str(path.parent)
if parent_path not in sys.path: if parent not in sys.path:
logging.debug("adding parent path to allow imports: %s", parent_path) logging.debug("adding parent path to allow imports: %s", parent)
sys.path.append(parent_path) sys.path.append(parent)
# retrieve potential service modules, and filter out invalid modules
base_module = os.path.basename(path)
module_names = os.listdir(path)
module_names = filter(lambda x: _valid_module(path, x), module_names)
module_names = map(lambda x: x[:-3], module_names)
# import and add all service modules in the path # import and add all service modules in the path
classes = [] classes = []
for module_name in module_names: for p in path.iterdir():
import_statement = f"{base_module}.{module_name}" if not _valid_module(p):
continue
import_statement = f"{path.name}.{p.stem}"
logging.debug("importing custom module: %s", import_statement) logging.debug("importing custom module: %s", import_statement)
try: try:
module = importlib.import_module(import_statement) module = importlib.import_module(import_statement)
@ -376,20 +360,19 @@ def load_classes(path: str, clazz: Generic[T]) -> T:
logging.exception( logging.exception(
"unexpected error during import, skipping: %s", import_statement "unexpected error during import, skipping: %s", import_statement
) )
return classes return classes
def load_logging_config(config_path: str) -> None: def load_logging_config(config_path: Path) -> None:
""" """
Load CORE logging configuration file. Load CORE logging configuration file.
:param config_path: path to logging config file :param config_path: path to logging config file
:return: nothing :return: nothing
""" """
with open(config_path, "r") as log_config_file: with config_path.open("r") as f:
log_config = json.load(log_config_file) log_config = json.load(f)
logging.config.dictConfig(log_config) logging.config.dictConfig(log_config)
def threadpool( def threadpool(

View file

@ -1,4 +1,5 @@
import logging import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar
from lxml import etree from lxml import etree
@ -25,7 +26,7 @@ T = TypeVar("T")
def write_xml_file( def write_xml_file(
xml_element: etree.Element, file_path: str, doctype: str = None xml_element: etree.Element, file_path: Path, doctype: str = None
) -> None: ) -> None:
xml_data = etree.tostring( xml_data = etree.tostring(
xml_element, xml_element,
@ -34,8 +35,8 @@ def write_xml_file(
encoding="UTF-8", encoding="UTF-8",
doctype=doctype, doctype=doctype,
) )
with open(file_path, "wb") as xml_file: with file_path.open("wb") as f:
xml_file.write(xml_data) f.write(xml_data)
def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T]: def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T]:
@ -293,13 +294,12 @@ class CoreXmlWriter:
self.write_session_metadata() self.write_session_metadata()
self.write_default_services() self.write_default_services()
def write(self, file_name: str) -> None: def write(self, path: Path) -> None:
self.scenario.set("name", file_name) self.scenario.set("name", str(path))
# write out generated xml # write out generated xml
xml_tree = etree.ElementTree(self.scenario) xml_tree = etree.ElementTree(self.scenario)
xml_tree.write( xml_tree.write(
file_name, xml_declaration=True, pretty_print=True, encoding="UTF-8" str(path), xml_declaration=True, pretty_print=True, encoding="UTF-8"
) )
def write_session_origin(self) -> None: def write_session_origin(self) -> None:
@ -580,8 +580,8 @@ class CoreXmlReader:
self.session: "Session" = session self.session: "Session" = session
self.scenario: Optional[etree.ElementTree] = None self.scenario: Optional[etree.ElementTree] = None
def read(self, file_name: str) -> None: def read(self, file_path: Path) -> None:
xml_tree = etree.parse(file_name) xml_tree = etree.parse(str(file_path))
self.scenario = xml_tree.getroot() self.scenario = xml_tree.getroot()
# read xml session content # read xml session content

View file

@ -1,5 +1,5 @@
import logging import logging
import os from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
@ -54,7 +54,7 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]:
def create_file( def create_file(
xml_element: etree.Element, xml_element: etree.Element,
doc_name: str, doc_name: str,
file_path: str, file_path: Path,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
""" """
@ -71,10 +71,11 @@ def create_file(
) )
if server: if server:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
corexml.write_xml_file(xml_element, temp.name, doctype=doctype) temp_path = Path(temp.name)
corexml.write_xml_file(xml_element, temp_path, doctype=doctype)
temp.close() temp.close()
server.remote_put(temp.name, file_path) server.remote_put(temp_path, file_path)
os.unlink(temp.name) temp_path.unlink()
else: else:
corexml.write_xml_file(xml_element, file_path, doctype=doctype) corexml.write_xml_file(xml_element, file_path, doctype=doctype)
@ -92,9 +93,9 @@ def create_node_file(
:return: :return:
""" """
if isinstance(node, CoreNode): if isinstance(node, CoreNode):
file_path = os.path.join(node.nodedir, file_name) file_path = node.nodedir / file_name
else: else:
file_path = os.path.join(node.session.session_dir, file_name) file_path = node.session.session_dir / file_name
create_file(xml_element, doc_name, file_path, node.server) create_file(xml_element, doc_name, file_path, node.server)
@ -316,7 +317,7 @@ def create_event_service_xml(
group: str, group: str,
port: str, port: str,
device: str, device: str,
file_directory: str, file_directory: Path,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
""" """
@ -340,8 +341,7 @@ def create_event_service_xml(
): ):
sub_element = etree.SubElement(event_element, name) sub_element = etree.SubElement(event_element, name)
sub_element.text = value sub_element.text = value
file_name = "libemaneeventservice.xml" file_path = file_directory / "libemaneeventservice.xml"
file_path = os.path.join(file_directory, file_name)
create_file(event_element, "emaneeventmsgsvc", file_path, server) create_file(event_element, "emaneeventmsgsvc", file_path, server)

View file

@ -12,6 +12,7 @@ import sys
import threading import threading
import time import time
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path
from core import constants from core import constants
from core.api.grpc.server import CoreGrpcServer from core.api.grpc.server import CoreGrpcServer
@ -148,7 +149,8 @@ def main():
:return: nothing :return: nothing
""" """
cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf") cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf")
load_logging_config(cfg["logfile"]) log_config_path = Path(cfg["logfile"])
load_logging_config(log_config_path)
banner() banner()
try: try:
cored(cfg) cored(cfg)

View file

@ -1,7 +1,7 @@
""" """
Unit tests for testing CORE EMANE networks. Unit tests for testing CORE EMANE networks.
""" """
import os from pathlib import Path
from tempfile import TemporaryFile from tempfile import TemporaryFile
from typing import Type from typing import Type
from xml.etree import ElementTree from xml.etree import ElementTree
@ -28,7 +28,8 @@ _EMANE_MODELS = [
EmaneCommEffectModel, EmaneCommEffectModel,
EmaneTdmaModel, EmaneTdmaModel,
] ]
_DIR = os.path.dirname(os.path.abspath(__file__)) _DIR: Path = Path(__file__).resolve().parent
_SCHEDULE: Path = _DIR / "../../examples/tdma/schedule.xml"
def ping( def ping(
@ -107,9 +108,7 @@ class TestEmane:
# configure tdma # configure tdma
if model == EmaneTdmaModel: if model == EmaneTdmaModel:
session.emane.set_model_config( session.emane.set_model_config(
emane_network.id, emane_network.id, EmaneTdmaModel.name, {"schedule": str(_SCHEDULE)}
EmaneTdmaModel.name,
{"schedule": os.path.join(_DIR, "../../examples/tdma/schedule.xml")},
) )
# create nodes # create nodes

View file

@ -1,3 +1,4 @@
from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
@ -68,7 +69,8 @@ class TestConfigServices:
service.create_dirs() service.create_dirs()
# then # then
node.privatedir.assert_called_with(MyService.directories[0]) directory = Path(MyService.directories[0])
node.privatedir.assert_called_with(directory)
def test_create_files_custom(self): def test_create_files_custom(self):
# given # given
@ -81,7 +83,8 @@ class TestConfigServices:
service.create_files() service.create_files()
# then # then
node.nodefile.assert_called_with(MyService.files[0], text) file_path = Path(MyService.files[0])
node.nodefile.assert_called_with(file_path, text)
def test_create_files_text(self): def test_create_files_text(self):
# given # given
@ -92,7 +95,8 @@ class TestConfigServices:
service.create_files() service.create_files()
# then # then
node.nodefile.assert_called_with(MyService.files[0], TEMPLATE_TEXT) file_path = Path(MyService.files[0])
node.nodefile.assert_called_with(file_path, TEMPLATE_TEXT)
def test_run_startup(self): def test_run_startup(self):
# given # given

View file

@ -2,9 +2,9 @@
Unit tests for testing basic CORE networks. Unit tests for testing basic CORE networks.
""" """
import os
import threading import threading
from typing import Type from pathlib import Path
from typing import List, Type
import pytest import pytest
@ -16,9 +16,9 @@ from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.network import HubNode, PtpNet, SwitchNode, WlanNode from core.nodes.network import HubNode, PtpNet, SwitchNode, WlanNode
_PATH = os.path.abspath(os.path.dirname(__file__)) _PATH: Path = Path(__file__).resolve().parent
_MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") _MOBILITY_FILE: Path = _PATH / "mobility.scen"
_WIRED = [PtpNet, HubNode, SwitchNode] _WIRED: List = [PtpNet, HubNode, SwitchNode]
def ping(from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes): def ping(from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes):
@ -195,7 +195,7 @@ class TestCore:
# configure mobility script for session # configure mobility script for session
config = { config = {
"file": _MOBILITY_FILE, "file": str(_MOBILITY_FILE),
"refresh_ms": "50", "refresh_ms": "50",
"loop": "1", "loop": "1",
"autostart": "0.0", "autostart": "0.0",

View file

@ -1,8 +1,8 @@
""" """
Tests for testing tlv message handling. Tests for testing tlv message handling.
""" """
import os
import time import time
from pathlib import Path
from typing import Optional from typing import Optional
import mock import mock
@ -425,7 +425,7 @@ class TestGui:
assert file_data == service_file.data assert file_data == service_file.data
def test_file_node_file_copy(self, request, coretlv: CoreHandler): def test_file_node_file_copy(self, request, coretlv: CoreHandler):
file_name = "/var/log/test/node.log" file_path = Path("/var/log/test/node.log")
node = coretlv.session.add_node(CoreNode) node = coretlv.session.add_node(CoreNode)
node.makenodedir() node.makenodedir()
file_data = "echo hello" file_data = "echo hello"
@ -433,7 +433,7 @@ class TestGui:
MessageFlags.ADD.value, MessageFlags.ADD.value,
[ [
(FileTlvs.NODE, node.id), (FileTlvs.NODE, node.id),
(FileTlvs.NAME, file_name), (FileTlvs.NAME, str(file_path)),
(FileTlvs.DATA, file_data), (FileTlvs.DATA, file_data),
], ],
) )
@ -441,10 +441,10 @@ class TestGui:
coretlv.handle_message(message) coretlv.handle_message(message)
if not request.config.getoption("mock"): if not request.config.getoption("mock"):
directory, basename = os.path.split(file_name) directory = str(file_path.parent)
created_directory = directory[1:].replace("/", ".") created_directory = directory[1:].replace("/", ".")
create_path = os.path.join(node.nodedir, created_directory, basename) create_path = node.nodedir / created_directory / file_path.name
assert os.path.exists(create_path) assert create_path.exists()
def test_exec_node_tty(self, coretlv: CoreHandler): def test_exec_node_tty(self, coretlv: CoreHandler):
coretlv.dispatch_replies = mock.MagicMock() coretlv.dispatch_replies = mock.MagicMock()
@ -547,20 +547,21 @@ class TestGui:
0, 0,
[(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)], [(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)],
) )
coretlv.handle_message(message) coretlv.handle_message(message)
assert Path(file_path).exists()
assert os.path.exists(file_path)
def test_event_open_xml(self, coretlv: CoreHandler, tmpdir): def test_event_open_xml(self, coretlv: CoreHandler, tmpdir):
xml_file = tmpdir.join("coretlv.session.xml") xml_file = tmpdir.join("coretlv.session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
node = coretlv.session.add_node(CoreNode) node = coretlv.session.add_node(CoreNode)
coretlv.session.save_xml(file_path) coretlv.session.save_xml(file_path)
coretlv.session.delete_node(node.id) coretlv.session.delete_node(node.id)
message = coreapi.CoreEventMessage.create( message = coreapi.CoreEventMessage.create(
0, 0,
[(EventTlvs.TYPE, EventTypes.FILE_OPEN.value), (EventTlvs.NAME, file_path)], [
(EventTlvs.TYPE, EventTypes.FILE_OPEN.value),
(EventTlvs.NAME, str(file_path)),
],
) )
coretlv.handle_message(message) coretlv.handle_message(message)

View file

@ -1,5 +1,5 @@
import itertools import itertools
import os from pathlib import Path
import pytest import pytest
from mock import MagicMock from mock import MagicMock
@ -9,8 +9,8 @@ from core.errors import CoreCommandError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager
_PATH = os.path.abspath(os.path.dirname(__file__)) _PATH: Path = Path(__file__).resolve().parent
_SERVICES_PATH = os.path.join(_PATH, "myservices") _SERVICES_PATH = _PATH / "myservices"
SERVICE_ONE = "MyService" SERVICE_ONE = "MyService"
SERVICE_TWO = "MyService2" SERVICE_TWO = "MyService2"
@ -64,15 +64,15 @@ class TestServices:
ServiceManager.add_services(_SERVICES_PATH) ServiceManager.add_services(_SERVICES_PATH)
my_service = ServiceManager.get(SERVICE_ONE) my_service = ServiceManager.get(SERVICE_ONE)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
file_name = my_service.configs[0] file_path = Path(my_service.configs[0])
file_path = node.hostfilename(file_name) file_path = node.host_path(file_path)
# when # when
session.services.create_service_files(node, my_service) session.services.create_service_files(node, my_service)
# then # then
if not request.config.getoption("mock"): if not request.config.getoption("mock"):
assert os.path.exists(file_path) assert file_path.exists()
def test_service_validate(self, session: Session): def test_service_validate(self, session: Session):
# given # given

View file

@ -1,3 +1,4 @@
from pathlib import Path
from tempfile import TemporaryFile from tempfile import TemporaryFile
from xml.etree import ElementTree from xml.etree import ElementTree
@ -34,7 +35,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -85,7 +86,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -148,7 +149,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -210,7 +211,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -261,7 +262,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -321,7 +322,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -390,7 +391,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed
@ -471,7 +472,7 @@ class TestXml:
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath file_path = Path(xml_file.strpath)
session.save_xml(file_path) session.save_xml(file_path)
# verify xml file was created and can be parsed # verify xml file was created and can be parsed