Compare commits
2 commits
master
...
feature/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f55432ba2 | ||
|
|
f72d0d8a69 |
603 changed files with 67077 additions and 26761 deletions
6
.github/workflows/daemon-checks.yml
vendored
6
.github/workflows/daemon-checks.yml
vendored
|
|
@ -4,13 +4,13 @@ on: [push]
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.6
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.6
|
||||
- name: install poetry
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
|||
21
.github/workflows/documentation.yml
vendored
21
.github/workflows/documentation.yml
vendored
|
|
@ -1,21 +0,0 @@
|
|||
name: documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
key: ${{ github.ref }}
|
||||
path: .cache
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -14,13 +14,9 @@ config.h.in
|
|||
config.log
|
||||
config.status
|
||||
configure
|
||||
configure~
|
||||
debian
|
||||
stamp-h1
|
||||
|
||||
# python virtual environments
|
||||
venv
|
||||
|
||||
# generated protobuf files
|
||||
*_pb2.py
|
||||
*_pb2_grpc.py
|
||||
|
|
@ -62,6 +58,3 @@ daemon/setup.py
|
|||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
# ignore core player files
|
||||
*.core
|
||||
|
|
|
|||
362
CHANGELOG.md
362
CHANGELOG.md
|
|
@ -1,365 +1,3 @@
|
|||
## 2023-08-01 CORE 9.0.3
|
||||
|
||||
* Installation
|
||||
* updated various dependencies
|
||||
* Documentation
|
||||
* improved GUI docs to include node interaction and note xhost usage
|
||||
* \#780 - fixed gRPC examples
|
||||
* \#787 - complete documentation revamp to leverage mkdocs material
|
||||
* \#790 - fixed custom emane model example
|
||||
* core-daemon
|
||||
* update type hinting to avoid deprecated imports
|
||||
* updated commands ran within docker based nodes to have proper environment variables
|
||||
* fixed issue improperly setting session options over gRPC
|
||||
* \#668 - add fedora sbin path to frr service
|
||||
* \#774 - fixed pcap configservice
|
||||
* \#805 - fixed radvd configservice template error
|
||||
* core-gui
|
||||
* update type hinting to avoid deprecated imports
|
||||
* fixed issue allowing duplicate named hook scripts
|
||||
* fixed issue joining sessions with RJ45 nodes
|
||||
* utility scripts
|
||||
* fixed issue in core-cleanup for removing devices
|
||||
|
||||
## 2023-03-02 CORE 9.0.2
|
||||
|
||||
* Installation
|
||||
* updated python dependencies, including invoke to resolve python 3.10+ issues
|
||||
* improved example dockerfiles to use less space for built images
|
||||
* Documentation
|
||||
* updated emane install instructions
|
||||
* added Docker related issues to install instructions
|
||||
* core-daemon
|
||||
* fixed issue using invalid device name in sysctl commands
|
||||
* updated PTP nodes to properly disable mac learning for their linux bridge
|
||||
* fixed issue for LXC nodes to properly use a configured image name and write it to XML
|
||||
* \#742 - fixed issue with bad wlan node id being used
|
||||
* \#744 - fixed issue not properly setting broadcast address
|
||||
* core-gui
|
||||
* fixed sample1.xml to remove SSH service
|
||||
* fixed emane demo examples
|
||||
* fixed issue displaying emane configs generally configured for a node
|
||||
|
||||
## 2022-11-28 CORE 9.0.1
|
||||
|
||||
* Installation
|
||||
* updated protobuf and grpcio-tools versions in pyproject.toml to account for bad version mix
|
||||
|
||||
## 2022-11-18 CORE 9.0.0
|
||||
|
||||
* Breaking Changes
|
||||
* removed session nodes file
|
||||
* removed session state file
|
||||
* emane now runs in one process per nem with unique control ports
|
||||
* grpc client has been refactored and updated
|
||||
* removed tcl/legacy gui, imn file support and the tlv api
|
||||
* link configuration is now different, but consistent, for wired links
|
||||
* Installation
|
||||
* added packaging for single file distribution
|
||||
* python3.9 is now the minimum required version
|
||||
* updated Dockerfile examples
|
||||
* updated various python dependencies
|
||||
* virtual environment is now installed to /opt/core/venv
|
||||
* Documentation
|
||||
* updated emane invoke task examples
|
||||
* revamped install documentation
|
||||
* added wireless node notes
|
||||
* core-gui
|
||||
* updated config services to display rendered templated and allow editing
|
||||
* fixed node icon issue when updating preferences
|
||||
* \#89 - throughput widget now works for hubs/switches
|
||||
* \#691 - fixed custom nodes to properly use config services
|
||||
* gRPC API
|
||||
* add linked call to support linking and unlinking interfaces without destroying them
|
||||
* fixed issue during start session clearing out session options
|
||||
* added call to get rendered config service files
|
||||
* removed get_node_links from links from client
|
||||
* nem id and nem port have been added to GetNode and AddLink calls
|
||||
* core-daemon
|
||||
* wired links always create two veth pairs joined by a bridge
|
||||
* node interfaces are now configured within the container to apply to outgoing traffic
|
||||
* session.add_node now uses NodeOptions, allowing for node specific options
|
||||
* fixed issue with xml reading node canvas values
|
||||
* removed Session.add_node_file
|
||||
* fixed get requirements logic
|
||||
* fixed docker/lxd node support terminal commands on remote servers
|
||||
* improved docker node command execution time using nsenter
|
||||
* new wireless node type added to support dynamic loss based on distance
|
||||
* \#513 - add and deleting distributed links during runtime is now supported
|
||||
* \#703 - fixed issue not starting emane event listening service
|
||||
|
||||
## 2022-03-21 CORE 8.2.0
|
||||
|
||||
* core-gui
|
||||
* improved failed starts to trigger runtime to allow node investigation
|
||||
* core-daemon
|
||||
* improved default service loading to use a full import path
|
||||
* updated session instantiation to always set to a runtime state
|
||||
* core-cli
|
||||
* \#672 - fixed xml loading
|
||||
* \#578 - restored json flag and added geo output to session overview
|
||||
* Documentation
|
||||
* updated emane example and documentation
|
||||
* improved table markdown
|
||||
|
||||
## 2022-02-18 CORE 8.1.0
|
||||
|
||||
* Installation
|
||||
* updated dependency versions to account for known vulnerabilities
|
||||
* GUI
|
||||
* fixed issue drawing asymmetric link configurations when joining a session
|
||||
* daemon
|
||||
* fixed issue getting templates and creating files for config services
|
||||
* added by directional support for network to network links
|
||||
* \#647 - fixed issue when creating RJ45 nodes
|
||||
* \#646 - fixed issue when creating files for Docker nodes
|
||||
* \#645 - improved wlan change updates to account for all updates with no delay
|
||||
* services
|
||||
* fixed file generation for OSPFv2 config service
|
||||
|
||||
## 2022-01-12 CORE 8.0.0
|
||||
|
||||
*Breaking Changes
|
||||
* heavily refactored gRPC client, removing some calls, adding others, all using type hinted classes representing their protobuf counterparts
|
||||
* emane adjustments to run each nem in its own process, includes adjustments to configuration, which may cause issues
|
||||
* internal daemon cleanup and refactoring, in a script directly driving a scenario is used
|
||||
* Installation
|
||||
* added options to allow installation without ospf mdr
|
||||
* removed tasks that are no longer needed
|
||||
* updates to properly install/remove example files
|
||||
* pipx/poetry/invoke versions are now locked to help avoid update related issues
|
||||
* install.sh is now setup.sh and is a convenience to get tool setup to run invoke
|
||||
* Documentation
|
||||
* formally added notes for Docker and LXD based node types
|
||||
* added config services
|
||||
* Updated README to have quick notes for installation
|
||||
* \#563 - update to note how to enable core service
|
||||
* Examples
|
||||
* \#598 - update to fix sample1.imn to working order
|
||||
* core-daemon
|
||||
* emane global configuration is now configurable per nem
|
||||
* fixed wlan loss to support float values
|
||||
* improved default service loading to use full core path
|
||||
* improved emane model loading to occur one time
|
||||
* fixed handling rj45 link edits from tlv api
|
||||
* fixed wlan config getting a default value for the promiscuous setting when not provided
|
||||
* ebtables usage has now been replaced with nftables
|
||||
* \#564 - logging is now using module named loggers
|
||||
* \#573 - emane processes are not created 1 to 1 with nems
|
||||
* \#608 - update lxml version
|
||||
* \#609 - update pyyaml version
|
||||
* \#623 - fixed issue with ovs mode and mac learning
|
||||
* core-gui
|
||||
* config services are now the default service type
|
||||
* legacy services are marked as deprecated
|
||||
* fix to properly load session options
|
||||
* logging is now using module named loggers
|
||||
* save as will not update the current session file name as expected
|
||||
* fix to properly clear out removed customized services
|
||||
* adding directories to a service that do not exist, is now valid
|
||||
* added flag to exit after creating gui directory from command line
|
||||
* added new options to enable/disable ip4/ip6 assignment
|
||||
* improved canvas draw order, when joining sessions
|
||||
* improved node copy/paste to avoid issues when pasting text into service config dialogs
|
||||
* each canvas will not correctly save and load their size from xml
|
||||
* gRPC API
|
||||
* session options are now returned for GetSession
|
||||
* fixed issue not properly creating the session directory during start session definition state
|
||||
* updates to separate editing a node and moving a node, new MoveNode call added, EditNode is now used for editing icons
|
||||
* Services
|
||||
* fixed default route config service
|
||||
* config services now have options for shadowing directories, including per node customization
|
||||
|
||||
## 2021-09-17 CORE 7.5.2
|
||||
|
||||
* Installation
|
||||
* \#596 - fixes issue related to installing poetry by pinning version to 1.1.7
|
||||
* updates pipx installation to pinned version 0.16.4
|
||||
* core-daemon
|
||||
* \#600 - fixes known vulnerability for pillow dependency by updating version
|
||||
|
||||
## 2021-04-15 CORE 7.5.1
|
||||
|
||||
* core-pygui
|
||||
* fixed issues creating and drawing custom nodes
|
||||
|
||||
## 2021-03-11 CORE 7.5.0
|
||||
|
||||
* core-daemon
|
||||
* fixed issue setting mobility loop value properly
|
||||
* fixed issue that some states would not properly remove session directories
|
||||
* \#560 - fixed issues with sdt integration for mobility movement and layer creation
|
||||
* core-pygui
|
||||
* added multiple canvas support
|
||||
* added support to hide nodes and restore them visually
|
||||
* update to assign full netmasks to wireless connected nodes by default
|
||||
* update to display services and action controls for nodes during runtime
|
||||
* fixed issues with custom nodes
|
||||
* fixed issue auto assigning macs, avoiding duplication
|
||||
* fixed issue joining session with different netmasks
|
||||
* fixed issues when deleting a session from the sessions dialog
|
||||
* \#550 - fixed issue not sending all service customization data
|
||||
* core-cli
|
||||
* added delete session command
|
||||
|
||||
## 2021-01-11 CORE 7.4.0
|
||||
|
||||
* Installation
|
||||
* fixed issue for automated install assuming ID_LIKE is always present in /etc/os-release
|
||||
* gRPC API
|
||||
* fixed issue stopping session and not properly going to data collect state
|
||||
* fixed issue to have start session properly create a directory before configuration state
|
||||
* core-pygui
|
||||
* fixed issue handling deletion of wired link to a switch
|
||||
* avoid saving edge metadata to xml when values are default
|
||||
* fixed issue editing node mac addresses
|
||||
* added support for configuring interface names
|
||||
* fixed issue with potential node names to allow hyphens and remove under bars
|
||||
* \#531 - fixed issue changing distributed nodes back to local
|
||||
* core-daemon
|
||||
* fixed issue to properly handle deleting links from a network to network node
|
||||
* updated xml to support writing and reading link buffer configurations
|
||||
* reverted change and removed mac learning from wlan, due to promiscuous like behavior
|
||||
* fixed issue creating control interfaces when starting services
|
||||
* fixed deadlock issue when clearing a session using sdt
|
||||
* \#116 - fixed issue for wlans handling multiple mobility scripts at once
|
||||
* \#539 - fixed issue in udp tlv api
|
||||
|
||||
## 2020-12-02 CORE 7.3.0
|
||||
|
||||
* core-daemon
|
||||
* fixed issue where emane global configuration was not being sent to core-gui
|
||||
* updated controlnet names on host to be prefixed with ctrl
|
||||
* fixed RJ45 link shutdown from core-gui causing an error
|
||||
* fixed emane external transport xml generation
|
||||
* \#517 - update to account for radvd required directory
|
||||
* \#514 - support added for session specific environment files
|
||||
* \#529 - updated to configure netem limit based on delay or user specified, requires kernel 3.3+
|
||||
* core-pygui
|
||||
* fixed issue drawing wlan/emane link options when it should not have
|
||||
* edge labels are now placed a set distance from nodes like original gui
|
||||
* link color/width are now saved to xml files
|
||||
* added support to configure buffer size for links
|
||||
* \#525 - added support for multiple wired links between the same nodes
|
||||
* \#526 - added option to hide/show links with 100% loss
|
||||
* Documentation
|
||||
* \#527 - typo in service documentation
|
||||
* \#515 - added examples to docs for using EMANE features within a CORE context
|
||||
|
||||
## 2020-09-29 CORE 7.2.1
|
||||
|
||||
* core-daemon
|
||||
* fixed issue where shutting down sessions may not have removed session directories
|
||||
* fixed issue with multiple emane interfaces on the same node not getting the right configuration
|
||||
* Installation
|
||||
* updated automated install to be a bit more robust for alternative distros
|
||||
* added force install type to try and leverage a redhat/debian like install
|
||||
* locked ospf mdr version installed to older commit to avoid issues with multiple interfaces on same node
|
||||
|
||||
## 2020-09-15 CORE 7.2.0
|
||||
|
||||
* Installation
|
||||
* locked down version of ospf-mdr installed in automated install
|
||||
* locked down version of emane to v1.2.5 in automated emane install
|
||||
* added option to install locally using the -l option
|
||||
* core-daemon
|
||||
* improve error when retrieving services that do not exist, or failed to load
|
||||
* fixed issue with writing/reading emane node interface configurations to xml
|
||||
* fixed issue with not setting the emane model when creating a node
|
||||
* added common utility method for getting a emane node interface config id in core.utils
|
||||
* fixed issue running emane on more than one interface for a node
|
||||
* fixed issue validating paths when creating emane transport xml for a node
|
||||
* fixed issue avoiding multiple calls to shutdown, if already in shutdown state
|
||||
* core-pygui
|
||||
* fixed issue configuring emane for a node interface
|
||||
* gRPC API
|
||||
* added wrapper client that can provide type hinting and a simpler interface at core.api.grpc.clientw
|
||||
* fixed issue creating sessions that default to having a very large reference scale
|
||||
* fixed issue with GetSession returning control net nodes
|
||||
|
||||
## 2020-08-21 CORE 7.1.0
|
||||
|
||||
* Installation
|
||||
* added core-python script that gets installed to help globally reference the virtual environment
|
||||
* gRPC API
|
||||
* GetSession will now return all configuration information for a session and the file it was opened from, if applicable
|
||||
* node update events will now include icon information
|
||||
* fixed issue with getting session throughputs for sessions with a high id
|
||||
* core-daemon
|
||||
* \#503 - EMANE networks will now work with mobility again
|
||||
* \#506 - fixed service dependency resolution issue
|
||||
* fixed issue sending hooks to core-gui when joining session
|
||||
* core-pygui
|
||||
* fixed issues editing hooks
|
||||
* fixed issue with cpu usage when joining a session
|
||||
* fixed mac field not being disabled during runtime when configuring a node
|
||||
* removed unlimited button from link config dialog
|
||||
* fixed issue with copy/paste links and their options
|
||||
* fixed issue with adding nodes/links and editing links during runtime
|
||||
* updated open file dialog in config dialogs to open to ~/.coregui home directory
|
||||
* fixed issue double clicking sessions dialog in invalid areas
|
||||
* added display of asymmetric link options on links
|
||||
* fixed emane config dialog display
|
||||
* fixed issue saving backgrounds in xml files
|
||||
* added view toggle for wired/wireless links
|
||||
* node events will now update icons
|
||||
|
||||
## 2020-07-28 CORE 7.0.1
|
||||
|
||||
* Bugfixes
|
||||
* \#500 - fixed issue running node commands with shell=True
|
||||
* fixed issue for poetry based install not properly vetting requirements for dataclasses dependency
|
||||
|
||||
## 2020-07-23 CORE 7.0.0
|
||||
|
||||
* Breaking Changes
|
||||
* core.emudata and core.data combined and cleaned up into core.data
|
||||
* updates to consistently use mac instead of hwaddr/mac
|
||||
* \#468 - code related to adding/editing/deleting links cleaned up
|
||||
* \#469 - usages of per all changed to loss to be consistent
|
||||
* \#470 - variables with numbered names now use numbers directly
|
||||
* \#471 - node startup is no longer embedded within its constructor
|
||||
* \#472 - code updated to refer to interfaces consistently as iface
|
||||
* \#475 - code updates changing how ip addresses are stored on interfaces
|
||||
* \#476 - executables to check for moved into own module core.executables
|
||||
* \#486 - core will now install into its own python virtual environment managed by poetry
|
||||
* core-daemon
|
||||
* updates to properly save/load distributed servers to xml
|
||||
* \#474 - added type hinting to all service files
|
||||
* \#478 - fixed typo in config service directory
|
||||
* \#479 - opening an xml file will now cycle through states like a normal session
|
||||
* \#480 - ovs configuration will now save/load from xml and display in guis
|
||||
* \#484 - changes to support adding emane links during runtime
|
||||
* core-pygui
|
||||
* fixed issue not displaying services for the default group in service dialogs
|
||||
* fixed issue starting a session when the daemon is not present
|
||||
* fixed issue attempting to open terminals for invalid nodes
|
||||
* fixed issue syncing session location
|
||||
* fixed issue joining a session with mobility, not in runtime
|
||||
* added cpu usage monitor to status bar
|
||||
* emane configurations can now be seen during runtime
|
||||
* rj45 nodes can only have one link
|
||||
* disabling throughputs will clear labels
|
||||
* improvements to custom service copy
|
||||
* link options will now be drawn on as a label
|
||||
* updates to handle runtime link events
|
||||
* \#477 - added optional details pane for a quick view of node/link details
|
||||
* \#485 - pygui fixed observer widget for invalid nodes
|
||||
* \#496 - improved alert handling
|
||||
* core-gui
|
||||
* \#493 - increased frame size to show all emane configuration options
|
||||
* gRPC API
|
||||
* added set session user rpc
|
||||
* added cpu usage stream
|
||||
* interface objects returned from get_node will now provide node_id, net_id, and net2_id data
|
||||
* peer to peer nodes will not be included in get_session calls
|
||||
* pathloss events will now throw an error when nem id not found
|
||||
* \#481 - link rpc calls will broadcast out
|
||||
* \#496 - added alert rpc call
|
||||
* Services
|
||||
* fixed issue reading files in security services
|
||||
* \#494 - add staticd to daemons list for frr services
|
||||
|
||||
## 2020-06-11 CORE 6.5.0
|
||||
* Breaking Changes
|
||||
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter
|
||||
|
|
|
|||
126
Dockerfile
126
Dockerfile
|
|
@ -1,126 +0,0 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:22.04
|
||||
LABEL Description="CORE Docker Ubuntu Image"
|
||||
|
||||
ARG PREFIX=/usr/local
|
||||
ARG BRANCH=master
|
||||
ARG PROTOC_VERSION=3.19.6
|
||||
ARG VENV_PATH=/opt/core/venv
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV PATH="$PATH:${VENV_PATH}/bin"
|
||||
WORKDIR /opt
|
||||
|
||||
# install system dependencies
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository "deb http://archive.ubuntu.com/ubuntu jammy universe"
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
automake \
|
||||
bash \
|
||||
ca-certificates \
|
||||
ethtool \
|
||||
gawk \
|
||||
gcc \
|
||||
g++ \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
libc-dev \
|
||||
libev-dev \
|
||||
libreadline-dev \
|
||||
libtool \
|
||||
nftables \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-tk \
|
||||
pkg-config \
|
||||
tk \
|
||||
xauth \
|
||||
xterm \
|
||||
wireshark \
|
||||
vim \
|
||||
build-essential \
|
||||
nano \
|
||||
firefox \
|
||||
net-tools \
|
||||
rsync \
|
||||
openssh-server \
|
||||
openssh-client \
|
||||
vsftpd \
|
||||
atftpd \
|
||||
atftp \
|
||||
mini-httpd \
|
||||
lynx \
|
||||
tcpdump \
|
||||
iperf \
|
||||
iperf3 \
|
||||
tshark \
|
||||
openssh-sftp-server \
|
||||
bind9 \
|
||||
bind9-utils \
|
||||
openvpn \
|
||||
isc-dhcp-server \
|
||||
isc-dhcp-client \
|
||||
whois \
|
||||
ipcalc \
|
||||
socat \
|
||||
hping3 \
|
||||
libgtk-3-0 \
|
||||
librest-0.7-0 \
|
||||
libgtk-3-common \
|
||||
dconf-gsettings-backend \
|
||||
libsoup-gnome2.4-1 \
|
||||
libsoup2.4-1 \
|
||||
dconf-service \
|
||||
x11-xserver-utils \
|
||||
ftp \
|
||||
git \
|
||||
sudo \
|
||||
wget \
|
||||
tzdata \
|
||||
libpcap-dev \
|
||||
libpcre3-dev \
|
||||
libprotobuf-dev \
|
||||
libxml2-dev \
|
||||
protobuf-compiler \
|
||||
unzip \
|
||||
uuid-dev \
|
||||
iproute2 \
|
||||
vlc \
|
||||
iputils-ping && \
|
||||
apt-get autoremove -y
|
||||
|
||||
# install core
|
||||
RUN git clone https://github.com/coreemu/core && \
|
||||
cd core && \
|
||||
git checkout ${BRANCH} && \
|
||||
./setup.sh && \
|
||||
PATH=/root/.local/bin:$PATH inv install -v -p ${PREFIX} && \
|
||||
cd /opt && \
|
||||
rm -rf ospf-mdr
|
||||
|
||||
# install emane
|
||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip && \
|
||||
mkdir protoc && \
|
||||
unzip protoc-${PROTOC_VERSION}-linux-x86_64.zip -d protoc && \
|
||||
git clone https://github.com/adjacentlink/emane.git && \
|
||||
cd emane && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix=/usr && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
cd src/python && \
|
||||
make clean && \
|
||||
PATH=/opt/protoc/bin:$PATH make && \
|
||||
${VENV_PATH}/bin/python -m pip install . && \
|
||||
cd /opt && \
|
||||
rm -rf protoc && \
|
||||
rm -rf emane && \
|
||||
rm -f protoc-${PROTOC_VERSION}-linux-x86_64.zip
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
CMD /opt/core/venv/bin/core-daemon
|
||||
91
Makefile.am
91
Makefile.am
|
|
@ -6,6 +6,10 @@ if WANT_DOCS
|
|||
DOCS = docs man
|
||||
endif
|
||||
|
||||
if WANT_GUI
|
||||
GUI = gui
|
||||
endif
|
||||
|
||||
if WANT_DAEMON
|
||||
DAEMON = daemon
|
||||
endif
|
||||
|
|
@ -15,13 +19,12 @@ if WANT_NETNS
|
|||
endif
|
||||
|
||||
# keep docs last due to dependencies on binaries
|
||||
SUBDIRS = $(DAEMON) $(NETNS) $(DOCS)
|
||||
SUBDIRS = $(GUI) $(DAEMON) $(NETNS) $(DOCS)
|
||||
|
||||
ACLOCAL_AMFLAGS = -I config
|
||||
|
||||
# extra files to include with distribution tarball
|
||||
EXTRA_DIST = bootstrap.sh \
|
||||
package \
|
||||
LICENSE \
|
||||
README.md \
|
||||
ASSIGNMENT_OF_COPYRIGHT.pdf \
|
||||
|
|
@ -48,19 +51,18 @@ fpm -s dir -t deb -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core-distributed_VERSION_ARCH.deb \
|
||||
-p core_distributed_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "nftables" \
|
||||
-d "ebtables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-distributed-rpm =
|
||||
|
|
@ -70,86 +72,23 @@ fpm -s dir -t rpm -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core-distributed_VERSION_ARCH.rpm \
|
||||
-p core_distributed_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "nftables" \
|
||||
-d "ebtables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-rpm =
|
||||
fpm -s dir -t rpm -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "core vnoded/vcmd and system dependencies" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--rpm-init package/core-daemon \
|
||||
--after-install package/after-install.sh \
|
||||
--after-remove package/after-remove.sh \
|
||||
-d "ethtool" \
|
||||
-d "tk" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "nftables" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/ \
|
||||
package/etc/core.conf=/etc/core/ \
|
||||
package/etc/logging.conf=/etc/core/ \
|
||||
package/examples=/opt/core/ \
|
||||
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
|
||||
endef
|
||||
|
||||
define fpm-deb =
|
||||
fpm -s dir -t deb -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "core vnoded/vcmd and system dependencies" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--deb-systemd package/core-daemon.service \
|
||||
--deb-no-default-config-files \
|
||||
--after-install package/after-install.sh \
|
||||
--after-remove package/after-remove.sh \
|
||||
-d "ethtool" \
|
||||
-d "tk" \
|
||||
-d "libtk-img" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "nftables" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/ \
|
||||
package/etc/core.conf=/etc/core/ \
|
||||
package/etc/logging.conf=/etc/core/ \
|
||||
package/examples=/opt/core/ \
|
||||
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
|
||||
endef
|
||||
|
||||
.PHONY: fpm
|
||||
fpm: clean-local-fpm
|
||||
cd daemon && poetry build -f wheel
|
||||
$(call fpm-deb)
|
||||
$(call fpm-rpm)
|
||||
.PHONY: fpm-distributed
|
||||
fpm-distributed: clean-local-fpm
|
||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||
$(call fpm-distributed-deb)
|
||||
$(call fpm-distributed-rpm)
|
||||
|
||||
|
|
@ -176,6 +115,7 @@ $(info creating file $1 from $1.in)
|
|||
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
|
||||
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \
|
||||
-e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \
|
||||
-e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \
|
||||
< $1.in > $1
|
||||
endef
|
||||
|
||||
|
|
@ -183,6 +123,7 @@ all: change-files
|
|||
|
||||
.PHONY: change-files
|
||||
change-files:
|
||||
$(call change-files,gui/core-gui)
|
||||
$(call change-files,daemon/core/constants.py)
|
||||
$(call change-files,netns/setup.py)
|
||||
|
||||
|
|
|
|||
109
README.md
109
README.md
|
|
@ -1,107 +1,24 @@
|
|||
# Index
|
||||
- CORE
|
||||
- Docker Setup
|
||||
- Precompiled container image
|
||||
- Build container image from source
|
||||
- Adding extra packages
|
||||
|
||||
- Useful commands
|
||||
- License
|
||||
|
||||
# CORE
|
||||
|
||||
CORE: Common Open Research Emulator
|
||||
|
||||
Copyright (c)2005-2022 the Boeing Company.
|
||||
Copyright (c)2005-2020 the Boeing Company.
|
||||
|
||||
See the LICENSE file included in this distribution.
|
||||
|
||||
# Docker Setup
|
||||
## About
|
||||
|
||||
Here you have 2 choices
|
||||
The Common Open Research Emulator (CORE) is a tool for emulating
|
||||
networks on one or more machines. You can connect these emulated
|
||||
networks to live networks. CORE consists of a GUI for drawing
|
||||
topologies of lightweight virtual machines, and Python modules for
|
||||
scripting network emulation.
|
||||
|
||||
## Precompiled container image
|
||||
## Documentation & Support
|
||||
|
||||
```bash
|
||||
We are leveraging GitHub hosted documentation and Discord for persistent
|
||||
chat rooms. This allows for more dynamic conversations and the
|
||||
capability to respond faster. Feel free to join us at the link below.
|
||||
|
||||
# Start container
|
||||
sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged --restart unless-stopped git.olympuslab.net/afonso/core-extra:latest
|
||||
|
||||
```
|
||||
## Build container image from source
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://gitea.olympuslab.net/afonso/core-extra.git
|
||||
|
||||
# cd into the directory
|
||||
cd core-extra
|
||||
|
||||
# build the docker image
|
||||
sudo docker build -t core-extra .
|
||||
|
||||
# start container
|
||||
sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged --restart unless-stopped core-extra
|
||||
|
||||
```
|
||||
|
||||
### Adding extra packages
|
||||
|
||||
To add extra packages you must modify the Dockerfile and then compile the docker image.
|
||||
If you install it after starting the container it will, by docker nature, be reverted on the next boot of the container.
|
||||
|
||||
# Useful commands
|
||||
|
||||
I have the following functions on my fish shell
|
||||
to help me better use core
|
||||
|
||||
THIS ONLY WORKS ON FISH, MODIFY FOR BASH OR ZSH
|
||||
|
||||
```fish
|
||||
|
||||
# RUN CORE GUI
|
||||
function core
|
||||
xhost +local:root
|
||||
sudo docker exec -it core core-gui
|
||||
end
|
||||
|
||||
# RUN BASH INSIDE THE CONTAINER
|
||||
function core-bash
|
||||
sudo docker exec -it core /bin/bash
|
||||
end
|
||||
|
||||
|
||||
# LAUNCH NODE BASH ON THE HOST MACHINE
|
||||
function launch-term --argument nodename
|
||||
sudo docker exec -it core xterm -bg black -fg white -fa 'DejaVu Sans Mono' -fs 16 -e vcmd -c /tmp/pycore.1/$nodename -- /bin/bash
|
||||
end
|
||||
|
||||
#TO RUN ANY OTHER COMMAND
|
||||
sudo docker exec -it core COMAND_GOES_HERE
|
||||
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
Copyright (c) 2005-2018, the Boeing Company.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* [Documentation](https://coreemu.github.io/core/)
|
||||
* [Discord Channel](https://discord.gg/AKd7kmP)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# (c)2010-2012 the Boeing Company
|
||||
#
|
||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
||||
#
|
||||
# Bootstrap the autoconf system.
|
||||
#
|
||||
|
||||
|
|
|
|||
74
configure.ac
74
configure.ac
|
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 9.0.3)
|
||||
AC_INIT(core, 6.5.0)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
|
@ -30,14 +30,25 @@ AC_SUBST(CORE_CONF_DIR)
|
|||
AC_SUBST(CORE_DATA_DIR)
|
||||
AC_SUBST(CORE_STATE_DIR)
|
||||
|
||||
# documentation option
|
||||
# CORE GUI configuration files and preferences in CORE_GUI_CONF_DIR
|
||||
# scenario files in ~/.core/configs/
|
||||
AC_ARG_WITH([guiconfdir],
|
||||
[AS_HELP_STRING([--with-guiconfdir=dir],
|
||||
[specify GUI configuration directory])],
|
||||
[CORE_GUI_CONF_DIR="$with_guiconfdir"],
|
||||
[CORE_GUI_CONF_DIR="\$\${HOME}/.core"])
|
||||
AC_SUBST(CORE_GUI_CONF_DIR)
|
||||
AC_ARG_ENABLE([gui],
|
||||
[AS_HELP_STRING([--enable-gui[=ARG]],
|
||||
[build and install the GUI (default is yes)])],
|
||||
[], [enable_gui=yes])
|
||||
AC_SUBST(enable_gui)
|
||||
AC_ARG_ENABLE([docs],
|
||||
[AS_HELP_STRING([--enable-docs[=ARG]],
|
||||
[build python documentation (default is no)])],
|
||||
[], [enable_docs=no])
|
||||
AC_SUBST(enable_docs)
|
||||
|
||||
# python option
|
||||
AC_ARG_ENABLE([python],
|
||||
[AS_HELP_STRING([--enable-python[=ARG]],
|
||||
[build and install the python bindings (default is yes)])],
|
||||
|
|
@ -83,7 +94,28 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
want_python=yes
|
||||
want_linux_netns=yes
|
||||
|
||||
AM_PATH_PYTHON(3.9)
|
||||
# Checks for libraries.
|
||||
AC_CHECK_LIB([netgraph], [NgMkSockNode])
|
||||
|
||||
# Checks for header files.
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_C_INLINE
|
||||
AC_TYPE_INT32_T
|
||||
AC_TYPE_PID_T
|
||||
AC_TYPE_SIZE_T
|
||||
AC_TYPE_SSIZE_T
|
||||
AC_TYPE_UINT32_T
|
||||
AC_TYPE_UINT8_T
|
||||
|
||||
# Checks for library functions.
|
||||
AC_FUNC_FORK
|
||||
AC_FUNC_MALLOC
|
||||
AC_FUNC_REALLOC
|
||||
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
|
||||
|
||||
AM_PATH_PYTHON(3.6)
|
||||
AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])])
|
||||
|
||||
AC_CHECK_PROG(sysctl_path, sysctl, $as_dir, no, $SEARCHPATH)
|
||||
|
|
@ -91,9 +123,9 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
AC_MSG_ERROR([Could not locate sysctl (from procps package).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(nftables_path, nft, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$nftables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate nftables (from nftables package).])
|
||||
AC_CHECK_PROG(ebtables_path, ebtables, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$ebtables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate ebtables (from ebtables package).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH)
|
||||
|
|
@ -139,25 +171,6 @@ fi
|
|||
|
||||
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
||||
want_linux_netns=yes
|
||||
|
||||
# Checks for header files.
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_C_INLINE
|
||||
AC_TYPE_INT32_T
|
||||
AC_TYPE_PID_T
|
||||
AC_TYPE_SIZE_T
|
||||
AC_TYPE_SSIZE_T
|
||||
AC_TYPE_UINT32_T
|
||||
AC_TYPE_UINT8_T
|
||||
|
||||
# Checks for library functions.
|
||||
AC_FUNC_FORK
|
||||
AC_FUNC_MALLOC
|
||||
AC_FUNC_REALLOC
|
||||
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
|
||||
|
||||
PKG_CHECK_MODULES(libev, libev,
|
||||
AC_MSG_RESULT([found libev using pkgconfig OK])
|
||||
AC_SUBST(libev_CFLAGS)
|
||||
|
|
@ -196,6 +209,7 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
|||
fi
|
||||
|
||||
# Variable substitutions
|
||||
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
||||
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
||||
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
||||
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
||||
|
|
@ -210,6 +224,9 @@ fi
|
|||
|
||||
# Output files
|
||||
AC_CONFIG_FILES([Makefile
|
||||
gui/version.tcl
|
||||
gui/Makefile
|
||||
gui/icons/Makefile
|
||||
man/Makefile
|
||||
docs/Makefile
|
||||
daemon/Makefile
|
||||
|
|
@ -231,12 +248,17 @@ Build:
|
|||
Prefix: ${prefix}
|
||||
Exec Prefix: ${exec_prefix}
|
||||
|
||||
GUI:
|
||||
GUI path: ${CORE_LIB_DIR}
|
||||
GUI config: ${CORE_GUI_CONF_DIR}
|
||||
|
||||
Daemon:
|
||||
Daemon path: ${bindir}
|
||||
Daemon config: ${CORE_CONF_DIR}
|
||||
Python: ${PYTHON}
|
||||
|
||||
Features to build:
|
||||
Build GUI: ${enable_gui}
|
||||
Build Daemon: ${enable_daemon}
|
||||
Documentation: ${want_docs}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
# CORE
|
||||
# (c)2010-2012 the Boeing Company.
|
||||
# See the LICENSE file included in this distribution.
|
||||
#
|
||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
||||
#
|
||||
# Makefile for building netns components.
|
||||
#
|
||||
|
|
@ -21,7 +25,10 @@ DISTCLEANFILES = Makefile.in
|
|||
|
||||
# files to include with distribution tarball
|
||||
EXTRA_DIST = core \
|
||||
data \
|
||||
doc/conf.py.in \
|
||||
examples \
|
||||
scripts \
|
||||
tests \
|
||||
setup.cfg \
|
||||
poetry.lock \
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@ import logging.config
|
|||
|
||||
# setup default null handler
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
# disable paramiko logging
|
||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,9 @@
|
|||
import logging
|
||||
from collections.abc import Iterable
|
||||
from queue import Empty, Queue
|
||||
from typing import Optional
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from core.api.grpc import core_pb2, grpcutils
|
||||
from core.api.grpc.grpcutils import convert_link_data
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.grpcutils import convert_link
|
||||
from core.emulator.data import (
|
||||
ConfigData,
|
||||
EventData,
|
||||
|
|
@ -15,21 +14,28 @@ from core.emulator.data import (
|
|||
)
|
||||
from core.emulator.session import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_node_event(session: Session, node_data: NodeData) -> core_pb2.Event:
|
||||
def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle node event when there is a node event
|
||||
|
||||
:param session: session node is from
|
||||
:param node_data: node data
|
||||
:return: node event that contains node id, name, model, position, and services
|
||||
"""
|
||||
node = node_data.node
|
||||
emane_configs = grpcutils.get_emane_model_configs_dict(session)
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs)
|
||||
x, y, _ = node.position.get()
|
||||
position = core_pb2.Position(x=x, y=y)
|
||||
lon, lat, alt = node.position.get_geo()
|
||||
geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
|
||||
services = [x.name for x in node.services]
|
||||
node_proto = core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
model=node.type,
|
||||
position=position,
|
||||
geo=geo,
|
||||
services=services,
|
||||
)
|
||||
message_type = node_data.message_type.value
|
||||
node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto)
|
||||
return core_pb2.Event(node_event=node_event, source=node_data.source)
|
||||
|
|
@ -42,7 +48,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
|||
:param link_data: link data
|
||||
:return: link event that has message type and link information
|
||||
"""
|
||||
link = convert_link_data(link_data)
|
||||
link = convert_link(link_data)
|
||||
message_type = link_data.message_type.value
|
||||
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
||||
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
||||
|
|
@ -180,7 +186,7 @@ class EventStreamer:
|
|||
try:
|
||||
data = self.queue.get(timeout=1)
|
||||
if isinstance(data, NodeData):
|
||||
event = handle_node_event(self.session, data)
|
||||
event = handle_node_event(data)
|
||||
elif isinstance(data, LinkData):
|
||||
event = handle_link_event(data)
|
||||
elif isinstance(data, EventData):
|
||||
|
|
@ -192,7 +198,7 @@ class EventStreamer:
|
|||
elif isinstance(data, FileData):
|
||||
event = handle_file_event(data)
|
||||
else:
|
||||
logger.error("unknown event: %s", data)
|
||||
logging.error("unknown event: %s", data)
|
||||
except Empty:
|
||||
pass
|
||||
if event:
|
||||
|
|
|
|||
|
|
@ -1,95 +1,52 @@
|
|||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any, Dict, List, Tuple, Type, Union
|
||||
|
||||
import grpc
|
||||
from grpc import ServicerContext
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import common_pb2, core_pb2, wrappers
|
||||
from core.api.grpc.configservices_pb2 import ConfigServiceConfig
|
||||
from core.api.grpc.emane_pb2 import NodeEmaneConfig
|
||||
from core.api.grpc.services_pb2 import (
|
||||
NodeServiceConfig,
|
||||
NodeServiceData,
|
||||
ServiceConfig,
|
||||
ServiceDefaults,
|
||||
)
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emane.nodes import EmaneNet, EmaneOptions
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.emulator.links import CoreLink
|
||||
from core.emulator.session import Session
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import (
|
||||
CoreNode,
|
||||
CoreNodeBase,
|
||||
CoreNodeOptions,
|
||||
NodeBase,
|
||||
NodeOptions,
|
||||
Position,
|
||||
)
|
||||
from core.nodes.docker import DockerNode, DockerOptions
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.lxd import LxcNode, LxcOptions
|
||||
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
|
||||
from core.nodes.podman import PodmanNode, PodmanOptions
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WORKERS = 10
|
||||
|
||||
|
||||
class CpuUsage:
|
||||
def __init__(self) -> None:
|
||||
self.stat_file: Path = Path("/proc/stat")
|
||||
self.prev_idle: int = 0
|
||||
self.prev_total: int = 0
|
||||
|
||||
def run(self) -> float:
|
||||
lines = self.stat_file.read_text().splitlines()[0]
|
||||
values = [int(x) for x in lines.split()[1:]]
|
||||
idle = sum(values[3:5])
|
||||
non_idle = sum(values[:3] + values[5:8])
|
||||
total = idle + non_idle
|
||||
total_diff = total - self.prev_total
|
||||
idle_diff = idle - self.prev_idle
|
||||
self.prev_idle = idle
|
||||
self.prev_total = total
|
||||
return (total_diff - idle_diff) / total_diff
|
||||
|
||||
|
||||
def add_node_data(
|
||||
_class: type[NodeBase], node_proto: core_pb2.Node
|
||||
) -> tuple[Position, NodeOptions]:
|
||||
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
|
||||
"""
|
||||
Convert node protobuf message to data for creating a node.
|
||||
|
||||
:param _class: node class to create options from
|
||||
:param node_proto: node proto message
|
||||
:return: node type, id, and options
|
||||
"""
|
||||
options = _class.create_options()
|
||||
options.icon = node_proto.icon
|
||||
options.canvas = node_proto.canvas
|
||||
if isinstance(options, CoreNodeOptions):
|
||||
options.model = node_proto.model
|
||||
options.services = node_proto.services
|
||||
options.config_services = node_proto.config_services
|
||||
if isinstance(options, EmaneOptions):
|
||||
options.emane_model = node_proto.emane
|
||||
if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
|
||||
options.image = node_proto.image
|
||||
position = Position()
|
||||
position.set(node_proto.position.x, node_proto.position.y)
|
||||
_id = node_proto.id
|
||||
_type = NodeTypes(node_proto.type)
|
||||
options = NodeOptions(
|
||||
name=node_proto.name,
|
||||
model=node_proto.model,
|
||||
icon=node_proto.icon,
|
||||
image=node_proto.image,
|
||||
services=node_proto.services,
|
||||
config_services=node_proto.config_services,
|
||||
)
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
if node_proto.server:
|
||||
options.server = node_proto.server
|
||||
position = node_proto.position
|
||||
options.set_position(position.x, position.y)
|
||||
if node_proto.HasField("geo"):
|
||||
geo = node_proto.geo
|
||||
position.set_geo(geo.lon, geo.lat, geo.alt)
|
||||
return position, options
|
||||
options.set_location(geo.lat, geo.lon, geo.alt)
|
||||
return _type, _id, options
|
||||
|
||||
|
||||
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
||||
|
|
@ -118,8 +75,8 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
|||
|
||||
|
||||
def add_link_data(
|
||||
link_proto: core_pb2.Link,
|
||||
) -> tuple[InterfaceData, InterfaceData, LinkOptions]:
|
||||
link_proto: core_pb2.Link
|
||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
|
||||
"""
|
||||
Convert link proto to link interfaces and options data.
|
||||
|
||||
|
|
@ -128,6 +85,7 @@ def add_link_data(
|
|||
"""
|
||||
iface1_data = link_iface(link_proto.iface1)
|
||||
iface2_data = link_iface(link_proto.iface2)
|
||||
link_type = LinkTypes(link_proto.type)
|
||||
options = LinkOptions()
|
||||
options_proto = link_proto.options
|
||||
if options_proto:
|
||||
|
|
@ -139,15 +97,14 @@ def add_link_data(
|
|||
options.mer = options_proto.mer
|
||||
options.burst = options_proto.burst
|
||||
options.mburst = options_proto.mburst
|
||||
options.buffer = options_proto.buffer
|
||||
options.unidirectional = options_proto.unidirectional
|
||||
options.key = options_proto.key
|
||||
return iface1_data, iface2_data, options
|
||||
return iface1_data, iface2_data, options, link_type
|
||||
|
||||
|
||||
def create_nodes(
|
||||
session: Session, node_protos: list[core_pb2.Node]
|
||||
) -> tuple[list[NodeBase], list[Exception]]:
|
||||
session: Session, node_protos: List[core_pb2.Node]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
"""
|
||||
Create nodes using a thread pool and wait for completion.
|
||||
|
||||
|
|
@ -157,28 +114,20 @@ def create_nodes(
|
|||
"""
|
||||
funcs = []
|
||||
for node_proto in node_protos:
|
||||
_type = NodeTypes(node_proto.type)
|
||||
_type, _id, options = add_node_data(node_proto)
|
||||
_class = session.get_node_class(_type)
|
||||
position, options = add_node_data(_class, node_proto)
|
||||
args = (
|
||||
_class,
|
||||
node_proto.id or None,
|
||||
node_proto.name or None,
|
||||
node_proto.server or None,
|
||||
position,
|
||||
options,
|
||||
)
|
||||
args = (_class, _id, options)
|
||||
funcs.append((session.add_node, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logger.debug("grpc created nodes time: %s", total)
|
||||
logging.debug("grpc created nodes time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
def create_links(
|
||||
session: Session, link_protos: list[core_pb2.Link]
|
||||
) -> tuple[list[NodeBase], list[Exception]]:
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
"""
|
||||
Create links using a thread pool and wait for completion.
|
||||
|
||||
|
|
@ -190,19 +139,19 @@ def create_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options)
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options, link_type)
|
||||
funcs.append((session.add_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logger.debug("grpc created links time: %s", total)
|
||||
logging.debug("grpc created links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
def edit_links(
|
||||
session: Session, link_protos: list[core_pb2.Link]
|
||||
) -> tuple[list[None], list[Exception]]:
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[None], List[Exception]]:
|
||||
"""
|
||||
Edit links using a thread pool and wait for completion.
|
||||
|
||||
|
|
@ -214,13 +163,13 @@ def edit_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options)
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
|
||||
funcs.append((session.update_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logger.debug("grpc edit links time: %s", total)
|
||||
logging.debug("grpc edit links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
|
|
@ -236,26 +185,10 @@ def convert_value(value: Any) -> str:
|
|||
return value
|
||||
|
||||
|
||||
def convert_session_options(session: Session) -> dict[str, common_pb2.ConfigOption]:
|
||||
config_options = {}
|
||||
for option in session.options.options:
|
||||
value = session.options.get(option.id)
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=option.label,
|
||||
name=option.id,
|
||||
value=value,
|
||||
type=option.type.value,
|
||||
select=option.options,
|
||||
group="Options",
|
||||
)
|
||||
config_options[option.id] = config_option
|
||||
return config_options
|
||||
|
||||
|
||||
def get_config_options(
|
||||
config: dict[str, str],
|
||||
configurable_options: Union[ConfigurableOptions, type[ConfigurableOptions]],
|
||||
) -> dict[str, common_pb2.ConfigOption]:
|
||||
config: Dict[str, str],
|
||||
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
"""
|
||||
Retrieve configuration options in a form that is used by the grpc server.
|
||||
|
||||
|
|
@ -265,7 +198,7 @@ def get_config_options(
|
|||
"""
|
||||
results = {}
|
||||
for configuration in configurable_options.configurations():
|
||||
value = config.get(configuration.id, configuration.default)
|
||||
value = config[configuration.id]
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=configuration.label,
|
||||
name=configuration.id,
|
||||
|
|
@ -283,15 +216,12 @@ def get_config_options(
|
|||
return results
|
||||
|
||||
|
||||
def get_node_proto(
|
||||
session: Session, node: NodeBase, emane_configs: list[NodeEmaneConfig]
|
||||
) -> core_pb2.Node:
|
||||
def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
||||
"""
|
||||
Convert CORE node to protobuf representation.
|
||||
|
||||
:param session: session containing node
|
||||
:param node: node to convert
|
||||
:param emane_configs: emane configs related to node
|
||||
:return: node proto
|
||||
"""
|
||||
node_type = session.get_node_type(node.__class__)
|
||||
|
|
@ -301,77 +231,24 @@ def get_node_proto(
|
|||
geo = core_pb2.Geo(
|
||||
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
|
||||
)
|
||||
services = [x.name for x in node.services]
|
||||
node_dir = None
|
||||
config_services = []
|
||||
if isinstance(node, CoreNodeBase):
|
||||
node_dir = str(node.directory)
|
||||
config_services = [x for x in node.config_services]
|
||||
channel = None
|
||||
if isinstance(node, CoreNode):
|
||||
channel = str(node.ctrlchnlname)
|
||||
services = getattr(node, "services", [])
|
||||
if services is None:
|
||||
services = []
|
||||
services = [x.name for x in services]
|
||||
config_services = getattr(node, "config_services", {})
|
||||
config_services = [x for x in config_services]
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.wireless_model.name
|
||||
image = None
|
||||
if isinstance(node, (DockerNode, LxcNode, PodmanNode)):
|
||||
image = node.image
|
||||
# check for wlan config
|
||||
wlan_config = session.mobility.get_configs(
|
||||
node.id, config_type=BasicRangeModel.name
|
||||
)
|
||||
if wlan_config:
|
||||
wlan_config = get_config_options(wlan_config, BasicRangeModel)
|
||||
# check for wireless config
|
||||
wireless_config = None
|
||||
if isinstance(node, WirelessNode):
|
||||
configs = node.get_config()
|
||||
wireless_config = {}
|
||||
for config in configs.values():
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=config.label,
|
||||
name=config.id,
|
||||
value=config.default,
|
||||
type=config.type.value,
|
||||
select=config.options,
|
||||
group=config.group,
|
||||
)
|
||||
wireless_config[config.id] = config_option
|
||||
# check for mobility config
|
||||
mobility_config = session.mobility.get_configs(
|
||||
node.id, config_type=Ns2ScriptedMobility.name
|
||||
)
|
||||
if mobility_config:
|
||||
mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility)
|
||||
# check for service configs
|
||||
custom_services = session.services.custom_services.get(node.id)
|
||||
service_configs = {}
|
||||
if custom_services:
|
||||
for service in custom_services.values():
|
||||
service_proto = get_service_configuration(service)
|
||||
service_configs[service.name] = NodeServiceConfig(
|
||||
node_id=node.id,
|
||||
service=service.name,
|
||||
data=service_proto,
|
||||
files=service.config_data,
|
||||
)
|
||||
# check for config service configs
|
||||
config_service_configs = {}
|
||||
if isinstance(node, CoreNode):
|
||||
for service in node.config_services.values():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_service_configs[service.name] = ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=service.name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
emane_model = node.model.name
|
||||
model = getattr(node, "type", None)
|
||||
node_dir = getattr(node, "nodedir", None)
|
||||
channel = getattr(node, "ctrlchnlname", None)
|
||||
image = getattr(node, "image", None)
|
||||
return core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
emane=emane_model,
|
||||
model=node.model,
|
||||
model=model,
|
||||
type=node_type.value,
|
||||
position=position,
|
||||
geo=geo,
|
||||
|
|
@ -381,94 +258,92 @@ def get_node_proto(
|
|||
config_services=config_services,
|
||||
dir=node_dir,
|
||||
channel=channel,
|
||||
canvas=node.canvas,
|
||||
wlan_config=wlan_config,
|
||||
wireless_config=wireless_config,
|
||||
mobility_config=mobility_config,
|
||||
service_configs=service_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
emane_configs=emane_configs,
|
||||
)
|
||||
|
||||
|
||||
def get_links(session: Session, node: NodeBase) -> list[core_pb2.Link]:
|
||||
def get_links(node: NodeBase):
|
||||
"""
|
||||
Retrieve a list of links for grpc to use.
|
||||
|
||||
:param session: session to get links for node
|
||||
:param node: node to get links from
|
||||
:return: protobuf links
|
||||
"""
|
||||
link_protos = []
|
||||
for core_link in session.link_manager.node_links(node):
|
||||
link_protos.extend(convert_core_link(core_link))
|
||||
if isinstance(node, (WlanNode, EmaneNet)):
|
||||
for link_data in node.links():
|
||||
link_protos.append(convert_link_data(link_data))
|
||||
return link_protos
|
||||
|
||||
|
||||
def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
Convert interface to protobuf.
|
||||
|
||||
:param iface: interface to convert
|
||||
:return: protobuf interface
|
||||
"""
|
||||
if isinstance(iface.node, CoreNetwork):
|
||||
return core_pb2.Interface(id=iface.id)
|
||||
else:
|
||||
ip4 = iface.get_ip4()
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip4 = str(ip4.ip) if ip4 else None
|
||||
ip6 = iface.get_ip6()
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
ip6 = str(ip6.ip) if ip6 else None
|
||||
mac = str(iface.mac) if iface.mac else None
|
||||
return core_pb2.Interface(
|
||||
id=iface.id,
|
||||
name=iface.name,
|
||||
mac=mac,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_core_link(core_link: CoreLink) -> list[core_pb2.Link]:
|
||||
"""
|
||||
Convert core link to protobuf data.
|
||||
|
||||
:param core_link: core link to convert
|
||||
:return: protobuf link data
|
||||
"""
|
||||
links = []
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
unidirectional = core_link.is_unidirectional()
|
||||
link = convert_link(node1, iface1, node2, iface2, iface1.options, unidirectional)
|
||||
links.append(link)
|
||||
if unidirectional:
|
||||
link = convert_link(
|
||||
node2, iface2, node1, iface1, iface2.options, unidirectional
|
||||
)
|
||||
links.append(link)
|
||||
for link in node.links():
|
||||
link_proto = convert_link(link)
|
||||
links.append(link_proto)
|
||||
return links
|
||||
|
||||
|
||||
def convert_link_data(link_data: LinkData) -> core_pb2.Link:
|
||||
def get_emane_model_id(node_id: int, iface_id: int) -> int:
|
||||
"""
|
||||
Get EMANE model id
|
||||
|
||||
:param node_id: node id
|
||||
:param iface_id: interface id
|
||||
:return: EMANE model id
|
||||
"""
|
||||
if iface_id >= 0:
|
||||
return node_id * 1000 + iface_id
|
||||
else:
|
||||
return node_id
|
||||
|
||||
|
||||
def parse_emane_model_id(_id: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Parses EMANE model id to get true node id and interface id.
|
||||
|
||||
:param _id: id to parse
|
||||
:return: node id and interface id
|
||||
"""
|
||||
iface_id = -1
|
||||
node_id = _id
|
||||
if _id >= 1000:
|
||||
iface_id = _id % 1000
|
||||
node_id = int(_id / 1000)
|
||||
return node_id, iface_id
|
||||
|
||||
|
||||
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||
return core_pb2.Interface(
|
||||
id=iface_data.id,
|
||||
name=iface_data.name,
|
||||
mac=iface_data.mac,
|
||||
ip4=iface_data.ip4,
|
||||
ip4_mask=iface_data.ip4_mask,
|
||||
ip6=iface_data.ip6,
|
||||
ip6_mask=iface_data.ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
|
||||
return core_pb2.LinkOptions(
|
||||
jitter=options_data.jitter,
|
||||
key=options_data.key,
|
||||
mburst=options_data.mburst,
|
||||
mer=options_data.mer,
|
||||
loss=options_data.loss,
|
||||
bandwidth=options_data.bandwidth,
|
||||
burst=options_data.burst,
|
||||
delay=options_data.delay,
|
||||
dup=options_data.dup,
|
||||
unidirectional=options_data.unidirectional,
|
||||
)
|
||||
|
||||
|
||||
def convert_link(link_data: LinkData) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link_data into core protobuf link.
|
||||
|
||||
:param link_data: link to convert
|
||||
:return: core protobuf Link
|
||||
"""
|
||||
iface1 = None
|
||||
if link_data.iface1 is not None:
|
||||
iface1 = convert_iface_data(link_data.iface1)
|
||||
iface1 = convert_iface(link_data.iface1)
|
||||
iface2 = None
|
||||
if link_data.iface2 is not None:
|
||||
iface2 = convert_iface_data(link_data.iface2)
|
||||
iface2 = convert_iface(link_data.iface2)
|
||||
options = convert_link_options(link_data.options)
|
||||
return core_pb2.Link(
|
||||
type=link_data.type.value,
|
||||
|
|
@ -483,132 +358,25 @@ def convert_link_data(link_data: LinkData) -> core_pb2.Link:
|
|||
)
|
||||
|
||||
|
||||
def convert_iface_data(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||
"""
|
||||
Convert interface data to protobuf.
|
||||
|
||||
:param iface_data: interface data to convert
|
||||
:return: interface protobuf
|
||||
"""
|
||||
return core_pb2.Interface(
|
||||
id=iface_data.id,
|
||||
name=iface_data.name,
|
||||
mac=iface_data.mac,
|
||||
ip4=iface_data.ip4,
|
||||
ip4_mask=iface_data.ip4_mask,
|
||||
ip6=iface_data.ip6,
|
||||
ip6_mask=iface_data.ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_link_options(options: LinkOptions) -> core_pb2.LinkOptions:
|
||||
"""
|
||||
Convert link options to protobuf.
|
||||
|
||||
:param options: link options to convert
|
||||
:return: link options protobuf
|
||||
"""
|
||||
return core_pb2.LinkOptions(
|
||||
jitter=options.jitter,
|
||||
key=options.key,
|
||||
mburst=options.mburst,
|
||||
mer=options.mer,
|
||||
loss=options.loss,
|
||||
bandwidth=options.bandwidth,
|
||||
burst=options.burst,
|
||||
delay=options.delay,
|
||||
dup=options.dup,
|
||||
buffer=options.buffer,
|
||||
unidirectional=options.unidirectional,
|
||||
)
|
||||
|
||||
|
||||
def convert_options_proto(options: core_pb2.LinkOptions) -> LinkOptions:
|
||||
return LinkOptions(
|
||||
delay=options.delay,
|
||||
bandwidth=options.bandwidth,
|
||||
loss=options.loss,
|
||||
dup=options.dup,
|
||||
jitter=options.jitter,
|
||||
mer=options.mer,
|
||||
burst=options.burst,
|
||||
mburst=options.mburst,
|
||||
buffer=options.buffer,
|
||||
unidirectional=options.unidirectional,
|
||||
key=options.key,
|
||||
)
|
||||
|
||||
|
||||
def convert_link(
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
options: LinkOptions,
|
||||
unidirectional: bool,
|
||||
) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link objects to link protobuf.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:param options: link options
|
||||
:param unidirectional: if this link is considered unidirectional
|
||||
:return: protobuf link
|
||||
"""
|
||||
if iface1 is not None:
|
||||
iface1 = convert_iface(iface1)
|
||||
if iface2 is not None:
|
||||
iface2 = convert_iface(iface2)
|
||||
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
|
||||
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
|
||||
if not (is_node1_wireless or is_node2_wireless):
|
||||
options = convert_link_options(options)
|
||||
options.unidirectional = unidirectional
|
||||
else:
|
||||
options = None
|
||||
return core_pb2.Link(
|
||||
type=LinkTypes.WIRED.value,
|
||||
node1_id=node1.id,
|
||||
node2_id=node2.id,
|
||||
iface1=iface1,
|
||||
iface2=iface2,
|
||||
options=options,
|
||||
network_id=None,
|
||||
label=None,
|
||||
color=None,
|
||||
)
|
||||
|
||||
|
||||
def parse_proc_net_dev(lines: list[str]) -> dict[str, dict[str, float]]:
|
||||
"""
|
||||
Parse lines of output from /proc/net/dev.
|
||||
|
||||
:param lines: lines of /proc/net/dev
|
||||
:return: parsed device to tx/rx values
|
||||
"""
|
||||
stats = {}
|
||||
for line in lines[2:]:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
line = line.split()
|
||||
line[0] = line[0].strip(":")
|
||||
stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])}
|
||||
return stats
|
||||
|
||||
|
||||
def get_net_stats() -> dict[str, dict[str, float]]:
|
||||
def get_net_stats() -> Dict[str, Dict]:
|
||||
"""
|
||||
Retrieve status about the current interfaces in the system
|
||||
|
||||
:return: send and receive status of the interfaces in the system
|
||||
"""
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
lines = f.readlines()[2:]
|
||||
return parse_proc_net_dev(lines)
|
||||
data = f.readlines()[2:]
|
||||
|
||||
stats = {}
|
||||
for line in data:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
line = line.split()
|
||||
line[0] = line[0].strip(":")
|
||||
stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])}
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def session_location(session: Session, location: core_pb2.SessionLocation) -> None:
|
||||
|
|
@ -667,14 +435,39 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
|
|||
)
|
||||
|
||||
|
||||
def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface:
|
||||
def iface_to_data(iface: CoreInterface) -> InterfaceData:
|
||||
ip4 = iface.get_ip4()
|
||||
ip4_addr = str(ip4.ip) if ip4 else None
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip6 = iface.get_ip6()
|
||||
ip6_addr = str(ip6.ip) if ip6 else None
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
return InterfaceData(
|
||||
id=iface.node_id,
|
||||
name=iface.name,
|
||||
mac=str(iface.mac),
|
||||
ip4=ip4_addr,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6_addr,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
Convenience for converting a core interface to the protobuf representation.
|
||||
|
||||
:param session: session interface belongs to
|
||||
:param node_id: id of node to convert interface for
|
||||
:param iface: interface to convert
|
||||
:return: interface proto
|
||||
"""
|
||||
if iface.node and iface.node.id == node_id:
|
||||
_id = iface.node_id
|
||||
else:
|
||||
_id = iface.net_id
|
||||
net_id = iface.net.id if iface.net else None
|
||||
node_id = iface.node.id if iface.node else None
|
||||
net2_id = iface.othernet.id if iface.othernet else None
|
||||
ip4_net = iface.get_ip4()
|
||||
ip4 = str(ip4_net.ip) if ip4_net else None
|
||||
ip4_mask = ip4_net.prefixlen if ip4_net else None
|
||||
|
|
@ -682,13 +475,11 @@ def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface
|
|||
ip6 = str(ip6_net.ip) if ip6_net else None
|
||||
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
||||
mac = str(iface.mac) if iface.mac else None
|
||||
nem_id = None
|
||||
nem_port = None
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
nem_id = session.emane.get_nem_id(iface)
|
||||
nem_port = session.emane.get_nem_port(iface)
|
||||
return core_pb2.Interface(
|
||||
id=iface.id,
|
||||
id=_id,
|
||||
net_id=net_id,
|
||||
net2_id=net2_id,
|
||||
node_id=node_id,
|
||||
name=iface.name,
|
||||
mac=mac,
|
||||
mtu=iface.mtu,
|
||||
|
|
@ -697,8 +488,6 @@ def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface
|
|||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
nem_id=nem_id,
|
||||
nem_port=nem_port,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -727,182 +516,3 @@ def get_nem_id(
|
|||
message = f"{node.name} interface {iface_id} nem id does not exist"
|
||||
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
|
||||
return nem_id
|
||||
|
||||
|
||||
def get_emane_model_configs_dict(session: Session) -> dict[int, list[NodeEmaneConfig]]:
|
||||
"""
|
||||
Get emane model configuration protobuf data.
|
||||
|
||||
:param session: session to get emane model configuration for
|
||||
:return: dict of emane model protobuf configurations
|
||||
"""
|
||||
configs = {}
|
||||
for _id, model_configs in session.emane.node_configs.items():
|
||||
for model_name in model_configs:
|
||||
model_class = session.emane.get_model(model_name)
|
||||
current_config = session.emane.get_config(_id, model_name)
|
||||
config = get_config_options(current_config, model_class)
|
||||
node_id, iface_id = utils.parse_iface_config_id(_id)
|
||||
iface_id = iface_id if iface_id is not None else -1
|
||||
node_config = NodeEmaneConfig(
|
||||
model=model_name, iface_id=iface_id, config=config
|
||||
)
|
||||
node_configs = configs.setdefault(node_id, [])
|
||||
node_configs.append(node_config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_hooks(session: Session) -> list[core_pb2.Hook]:
|
||||
"""
|
||||
Retrieve hook protobuf data for a session.
|
||||
|
||||
:param session: session to get hooks for
|
||||
:return: list of hook protobufs
|
||||
"""
|
||||
hooks = []
|
||||
for state in session.hooks:
|
||||
state_hooks = session.hooks[state]
|
||||
for file_name, file_data in state_hooks:
|
||||
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
|
||||
hooks.append(hook)
|
||||
return hooks
|
||||
|
||||
|
||||
def get_default_services(session: Session) -> list[ServiceDefaults]:
|
||||
"""
|
||||
Retrieve the default service sets for a given session.
|
||||
|
||||
:param session: session to get default service sets for
|
||||
:return: list of default service sets
|
||||
"""
|
||||
default_services = []
|
||||
for model, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(model=model, services=services)
|
||||
default_services.append(default_service)
|
||||
return default_services
|
||||
|
||||
|
||||
def get_mobility_node(
|
||||
session: Session, node_id: int, context: ServicerContext
|
||||
) -> Union[WlanNode, EmaneNet]:
|
||||
"""
|
||||
Get mobility node.
|
||||
|
||||
:param session: session to get node from
|
||||
:param node_id: id of node to get
|
||||
:param context: grpc context
|
||||
:return: wlan or emane node
|
||||
"""
|
||||
try:
|
||||
return session.get_node(node_id, WlanNode)
|
||||
except CoreError:
|
||||
try:
|
||||
return session.get_node(node_id, EmaneNet)
|
||||
except CoreError:
|
||||
context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane")
|
||||
|
||||
|
||||
def convert_session(session: Session) -> wrappers.Session:
|
||||
"""
|
||||
Convert session to its wrapped version.
|
||||
|
||||
:param session: session to convert
|
||||
:return: wrapped session data
|
||||
"""
|
||||
emane_configs = get_emane_model_configs_dict(session)
|
||||
nodes = []
|
||||
links = []
|
||||
for _id in session.nodes:
|
||||
node = session.nodes[_id]
|
||||
if not isinstance(node, (PtpNet, CtrlNet)):
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
node_proto = get_node_proto(session, node, node_emane_configs)
|
||||
nodes.append(node_proto)
|
||||
if isinstance(node, (WlanNode, EmaneNet)):
|
||||
for link_data in node.links():
|
||||
links.append(convert_link_data(link_data))
|
||||
for core_link in session.link_manager.links():
|
||||
links.extend(convert_core_link(core_link))
|
||||
default_services = get_default_services(session)
|
||||
x, y, z = session.location.refxyz
|
||||
lat, lon, alt = session.location.refgeo
|
||||
location = core_pb2.SessionLocation(
|
||||
x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale
|
||||
)
|
||||
hooks = get_hooks(session)
|
||||
session_file = str(session.file_path) if session.file_path else None
|
||||
options = convert_session_options(session)
|
||||
servers = [
|
||||
core_pb2.Server(name=x.name, host=x.host)
|
||||
for x in session.distributed.servers.values()
|
||||
]
|
||||
return core_pb2.Session(
|
||||
id=session.id,
|
||||
state=session.state.value,
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
dir=str(session.directory),
|
||||
user=session.user,
|
||||
default_services=default_services,
|
||||
location=location,
|
||||
hooks=hooks,
|
||||
metadata=session.metadata,
|
||||
file=session_file,
|
||||
options=options,
|
||||
servers=servers,
|
||||
)
|
||||
|
||||
|
||||
def configure_node(
|
||||
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
|
||||
) -> None:
|
||||
"""
|
||||
Configure a node using all provided protobuf data.
|
||||
|
||||
:param session: session for node
|
||||
:param node: node protobuf data
|
||||
:param core_node: session node
|
||||
:param context: grpc context
|
||||
:return: nothing
|
||||
"""
|
||||
for emane_config in node.emane_configs:
|
||||
_id = utils.iface_config_id(node.id, emane_config.iface_id)
|
||||
config = {k: v.value for k, v in emane_config.config.items()}
|
||||
session.emane.set_config(_id, emane_config.model, config)
|
||||
if node.wlan_config:
|
||||
config = {k: v.value for k, v in node.wlan_config.items()}
|
||||
session.mobility.set_model_config(node.id, BasicRangeModel.name, config)
|
||||
if node.mobility_config:
|
||||
config = {k: v.value for k, v in node.mobility_config.items()}
|
||||
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config)
|
||||
if isinstance(core_node, WirelessNode) and node.wireless_config:
|
||||
config = {k: v.value for k, v in node.wireless_config.items()}
|
||||
core_node.set_config(config)
|
||||
for service_name, service_config in node.service_configs.items():
|
||||
data = service_config.data
|
||||
config = ServiceConfig(
|
||||
node_id=node.id,
|
||||
service=service_name,
|
||||
startup=data.startup,
|
||||
validate=data.validate,
|
||||
shutdown=data.shutdown,
|
||||
files=data.configs,
|
||||
directories=data.dirs,
|
||||
)
|
||||
service_configuration(session, config)
|
||||
for file_name, file_data in service_config.files.items():
|
||||
session.services.set_service_file(
|
||||
node.id, service_name, file_name, file_data
|
||||
)
|
||||
if node.config_service_configs:
|
||||
if not isinstance(core_node, CoreNode):
|
||||
context.abort(
|
||||
grpc.StatusCode.INVALID_ARGUMENT,
|
||||
"invalid node type with config service configs",
|
||||
)
|
||||
for service_name, service_config in node.config_service_configs.items():
|
||||
service = core_node.config_services[service_name]
|
||||
if service_config.config:
|
||||
service.set_config(service_config.config)
|
||||
for name, template in service_config.templates.items():
|
||||
service.set_template(name, template)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1016
daemon/core/api/tlv/coreapi.py
Normal file
1016
daemon/core/api/tlv/coreapi.py
Normal file
File diff suppressed because it is too large
Load diff
2079
daemon/core/api/tlv/corehandlers.py
Normal file
2079
daemon/core/api/tlv/corehandlers.py
Normal file
File diff suppressed because it is too large
Load diff
60
daemon/core/api/tlv/coreserver.py
Normal file
60
daemon/core/api/tlv/coreserver.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Defines core server for handling TCP connections.
|
||||
"""
|
||||
|
||||
import socketserver
|
||||
|
||||
from core.emulator.coreemu import CoreEmu
|
||||
|
||||
|
||||
class CoreServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
"""
|
||||
TCP server class, manages sessions and spawns request handlers for
|
||||
incoming connections.
|
||||
"""
|
||||
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, config=None):
|
||||
"""
|
||||
Server class initialization takes configuration data and calls
|
||||
the socketserver constructor.
|
||||
|
||||
:param tuple[str, int] server_address: server host and port to use
|
||||
:param class handler_class: request handler
|
||||
:param dict config: configuration setting
|
||||
"""
|
||||
self.coreemu = CoreEmu(config)
|
||||
self.config = config
|
||||
socketserver.TCPServer.__init__(self, server_address, handler_class)
|
||||
|
||||
|
||||
class CoreUdpServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
|
||||
"""
|
||||
UDP server class, manages sessions and spawns request handlers for
|
||||
incoming connections.
|
||||
"""
|
||||
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, mainserver):
|
||||
"""
|
||||
Server class initialization takes configuration data and calls
|
||||
the SocketServer constructor
|
||||
|
||||
:param server_address:
|
||||
:param class handler_class: request handler
|
||||
:param mainserver:
|
||||
"""
|
||||
self.mainserver = mainserver
|
||||
socketserver.UDPServer.__init__(self, server_address, handler_class)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Thread target to run concurrently with the TCP server.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.serve_forever()
|
||||
176
daemon/core/api/tlv/dataconversion.py
Normal file
176
daemon/core/api/tlv/dataconversion.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""
|
||||
Converts CORE data objects into legacy API messages.
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List
|
||||
|
||||
from core.api.tlv import coreapi, structutils
|
||||
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
|
||||
from core.config import ConfigGroup, ConfigurableOptions
|
||||
from core.emulator.data import ConfigData, NodeData
|
||||
|
||||
|
||||
def convert_node(node_data: NodeData):
|
||||
"""
|
||||
Convenience method for converting NodeData to a packed TLV message.
|
||||
|
||||
:param core.emulator.data.NodeData node_data: node data to convert
|
||||
:return: packed node message
|
||||
"""
|
||||
node = node_data.node
|
||||
services = None
|
||||
if node.services is not None:
|
||||
services = "|".join([x.name for x in node.services])
|
||||
server = None
|
||||
if node.server is not None:
|
||||
server = node.server.name
|
||||
tlv_data = structutils.pack_values(
|
||||
coreapi.CoreNodeTlv,
|
||||
[
|
||||
(NodeTlvs.NUMBER, node.id),
|
||||
(NodeTlvs.TYPE, node.apitype.value),
|
||||
(NodeTlvs.NAME, node.name),
|
||||
(NodeTlvs.MODEL, node.type),
|
||||
(NodeTlvs.EMULATION_SERVER, server),
|
||||
(NodeTlvs.X_POSITION, int(node.position.x)),
|
||||
(NodeTlvs.Y_POSITION, int(node.position.y)),
|
||||
(NodeTlvs.CANVAS, node.canvas),
|
||||
(NodeTlvs.SERVICES, services),
|
||||
(NodeTlvs.LATITUDE, str(node.position.lat)),
|
||||
(NodeTlvs.LONGITUDE, str(node.position.lon)),
|
||||
(NodeTlvs.ALTITUDE, str(node.position.alt)),
|
||||
(NodeTlvs.ICON, node.icon),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
|
||||
|
||||
|
||||
def convert_config(config_data):
|
||||
"""
|
||||
Convenience method for converting ConfigData to a packed TLV message.
|
||||
|
||||
:param core.emulator.data.ConfigData config_data: config data to convert
|
||||
:return: packed message
|
||||
"""
|
||||
session = None
|
||||
if config_data.session is not None:
|
||||
session = str(config_data.session)
|
||||
tlv_data = structutils.pack_values(
|
||||
coreapi.CoreConfigTlv,
|
||||
[
|
||||
(ConfigTlvs.NODE, config_data.node),
|
||||
(ConfigTlvs.OBJECT, config_data.object),
|
||||
(ConfigTlvs.TYPE, config_data.type),
|
||||
(ConfigTlvs.DATA_TYPES, config_data.data_types),
|
||||
(ConfigTlvs.VALUES, config_data.data_values),
|
||||
(ConfigTlvs.CAPTIONS, config_data.captions),
|
||||
(ConfigTlvs.BITMAP, config_data.bitmap),
|
||||
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
|
||||
(ConfigTlvs.GROUPS, config_data.groups),
|
||||
(ConfigTlvs.SESSION, session),
|
||||
(ConfigTlvs.IFACE_ID, config_data.iface_id),
|
||||
(ConfigTlvs.NETWORK_ID, config_data.network_id),
|
||||
(ConfigTlvs.OPAQUE, config_data.opaque),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreConfMessage.pack(config_data.message_type, tlv_data)
|
||||
|
||||
|
||||
class ConfigShim:
|
||||
"""
|
||||
Provides helper methods for converting newer configuration values into TLV
|
||||
compatible formats.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def str_to_dict(cls, key_values: str) -> Dict[str, str]:
|
||||
"""
|
||||
Converts a TLV key/value string into an ordered mapping.
|
||||
|
||||
:param key_values:
|
||||
:return: ordered mapping of key/value pairs
|
||||
"""
|
||||
key_values = key_values.split("|")
|
||||
values = OrderedDict()
|
||||
for key_value in key_values:
|
||||
key, value = key_value.split("=", 1)
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str:
|
||||
"""
|
||||
Converts configuration groups to a TLV formatted string.
|
||||
|
||||
:param config_groups: configuration groups to format
|
||||
:return: TLV configuration group string
|
||||
"""
|
||||
group_strings = []
|
||||
for config_group in config_groups:
|
||||
group_string = (
|
||||
f"{config_group.name}:{config_group.start}-{config_group.stop}"
|
||||
)
|
||||
group_strings.append(group_string)
|
||||
return "|".join(group_strings)
|
||||
|
||||
@classmethod
|
||||
def config_data(
|
||||
cls,
|
||||
flags: int,
|
||||
node_id: int,
|
||||
type_flags: int,
|
||||
configurable_options: ConfigurableOptions,
|
||||
config: Dict[str, str],
|
||||
) -> ConfigData:
|
||||
"""
|
||||
Convert this class to a Config API message. Some TLVs are defined
|
||||
by the class, but node number, conf type flags, and values must
|
||||
be passed in.
|
||||
|
||||
:param flags: message flags
|
||||
:param node_id: node id
|
||||
:param type_flags: type flags
|
||||
:param configurable_options: options to create config data for
|
||||
:param config: configuration values for options
|
||||
:return: configuration data object
|
||||
"""
|
||||
key_values = None
|
||||
captions = None
|
||||
data_types = []
|
||||
possible_values = []
|
||||
logging.debug("configurable: %s", configurable_options)
|
||||
logging.debug("configuration options: %s", configurable_options.configurations)
|
||||
logging.debug("configuration data: %s", config)
|
||||
for configuration in configurable_options.configurations():
|
||||
if not captions:
|
||||
captions = configuration.label
|
||||
else:
|
||||
captions += f"|{configuration.label}"
|
||||
|
||||
data_types.append(configuration.type.value)
|
||||
|
||||
options = ",".join(configuration.options)
|
||||
possible_values.append(options)
|
||||
|
||||
_id = configuration.id
|
||||
config_value = config.get(_id, configuration.default)
|
||||
key_value = f"{_id}={config_value}"
|
||||
if not key_values:
|
||||
key_values = key_value
|
||||
else:
|
||||
key_values += f"|{key_value}"
|
||||
|
||||
groups_str = cls.groups_to_str(configurable_options.config_groups())
|
||||
return ConfigData(
|
||||
message_type=flags,
|
||||
node=node_id,
|
||||
object=configurable_options.name,
|
||||
type=type_flags,
|
||||
data_types=tuple(data_types),
|
||||
data_values=key_values,
|
||||
captions=captions,
|
||||
possible_values="|".join(possible_values),
|
||||
bitmap=configurable_options.bitmap,
|
||||
groups=groups_str,
|
||||
)
|
||||
212
daemon/core/api/tlv/enumerations.py
Normal file
212
daemon/core/api/tlv/enumerations.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"""
|
||||
Enumerations specific to the CORE TLV API.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
CORE_API_PORT = 4038
|
||||
|
||||
|
||||
class MessageTypes(Enum):
|
||||
"""
|
||||
CORE message types.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
LINK = 0x02
|
||||
EXECUTE = 0x03
|
||||
REGISTER = 0x04
|
||||
CONFIG = 0x05
|
||||
FILE = 0x06
|
||||
INTERFACE = 0x07
|
||||
EVENT = 0x08
|
||||
SESSION = 0x09
|
||||
EXCEPTION = 0x0A
|
||||
|
||||
|
||||
class NodeTlvs(Enum):
|
||||
"""
|
||||
Node type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NUMBER = 0x01
|
||||
TYPE = 0x02
|
||||
NAME = 0x03
|
||||
IP_ADDRESS = 0x04
|
||||
MAC_ADDRESS = 0x05
|
||||
IP6_ADDRESS = 0x06
|
||||
MODEL = 0x07
|
||||
EMULATION_SERVER = 0x08
|
||||
SESSION = 0x0A
|
||||
X_POSITION = 0x20
|
||||
Y_POSITION = 0x21
|
||||
CANVAS = 0x22
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
SERVICES = 0x25
|
||||
LATITUDE = 0x30
|
||||
LONGITUDE = 0x31
|
||||
ALTITUDE = 0x32
|
||||
ICON = 0x42
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class LinkTlvs(Enum):
|
||||
"""
|
||||
Link type, length, value enumerations.
|
||||
"""
|
||||
|
||||
N1_NUMBER = 0x01
|
||||
N2_NUMBER = 0x02
|
||||
DELAY = 0x03
|
||||
BANDWIDTH = 0x04
|
||||
LOSS = 0x05
|
||||
DUP = 0x06
|
||||
JITTER = 0x07
|
||||
MER = 0x08
|
||||
BURST = 0x09
|
||||
SESSION = 0x0A
|
||||
MBURST = 0x10
|
||||
TYPE = 0x20
|
||||
GUI_ATTRIBUTES = 0x21
|
||||
UNIDIRECTIONAL = 0x22
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
KEY = 0x25
|
||||
IFACE1_NUMBER = 0x30
|
||||
IFACE1_IP4 = 0x31
|
||||
IFACE1_IP4_MASK = 0x32
|
||||
IFACE1_MAC = 0x33
|
||||
IFACE1_IP6 = 0x34
|
||||
IFACE1_IP6_MASK = 0x35
|
||||
IFACE2_NUMBER = 0x36
|
||||
IFACE2_IP4 = 0x37
|
||||
IFACE2_IP4_MASK = 0x38
|
||||
IFACE2_MAC = 0x39
|
||||
IFACE2_IP6 = 0x40
|
||||
IFACE2_IP6_MASK = 0x41
|
||||
INTERFACE1_NAME = 0x42
|
||||
INTERFACE2_NAME = 0x43
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class ExecuteTlvs(Enum):
|
||||
"""
|
||||
Execute type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NUMBER = 0x02
|
||||
TIME = 0x03
|
||||
COMMAND = 0x04
|
||||
RESULT = 0x05
|
||||
STATUS = 0x06
|
||||
SESSION = 0x0A
|
||||
|
||||
|
||||
class ConfigTlvs(Enum):
|
||||
"""
|
||||
Configuration type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
OBJECT = 0x02
|
||||
TYPE = 0x03
|
||||
DATA_TYPES = 0x04
|
||||
VALUES = 0x05
|
||||
CAPTIONS = 0x06
|
||||
BITMAP = 0x07
|
||||
POSSIBLE_VALUES = 0x08
|
||||
GROUPS = 0x09
|
||||
SESSION = 0x0A
|
||||
IFACE_ID = 0x0B
|
||||
NETWORK_ID = 0x24
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class FileTlvs(Enum):
|
||||
"""
|
||||
File type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NAME = 0x02
|
||||
MODE = 0x03
|
||||
NUMBER = 0x04
|
||||
TYPE = 0x05
|
||||
SOURCE_NAME = 0x06
|
||||
SESSION = 0x0A
|
||||
DATA = 0x10
|
||||
COMPRESSED_DATA = 0x11
|
||||
|
||||
|
||||
class InterfaceTlvs(Enum):
|
||||
"""
|
||||
Interface type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NUMBER = 0x02
|
||||
NAME = 0x03
|
||||
IP_ADDRESS = 0x04
|
||||
MASK = 0x05
|
||||
MAC_ADDRESS = 0x06
|
||||
IP6_ADDRESS = 0x07
|
||||
IP6_MASK = 0x08
|
||||
TYPE = 0x09
|
||||
SESSION = 0x0A
|
||||
STATE = 0x0B
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
|
||||
|
||||
class EventTlvs(Enum):
|
||||
"""
|
||||
Event type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
TYPE = 0x02
|
||||
NAME = 0x03
|
||||
DATA = 0x04
|
||||
TIME = 0x05
|
||||
SESSION = 0x0A
|
||||
|
||||
|
||||
class SessionTlvs(Enum):
|
||||
"""
|
||||
Session type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NUMBER = 0x01
|
||||
NAME = 0x02
|
||||
FILE = 0x03
|
||||
NODE_COUNT = 0x04
|
||||
DATE = 0x05
|
||||
THUMB = 0x06
|
||||
USER = 0x07
|
||||
OPAQUE = 0x0A
|
||||
|
||||
|
||||
class ExceptionTlvs(Enum):
|
||||
"""
|
||||
Exception type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
SESSION = 0x02
|
||||
LEVEL = 0x03
|
||||
SOURCE = 0x04
|
||||
DATE = 0x05
|
||||
TEXT = 0x06
|
||||
OPAQUE = 0x0A
|
||||
43
daemon/core/api/tlv/structutils.py
Normal file
43
daemon/core/api/tlv/structutils.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
Utilities for working with python struct data.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def pack_values(clazz, packers):
|
||||
"""
|
||||
Pack values for a given legacy class.
|
||||
|
||||
:param class clazz: class that will provide a pack method
|
||||
:param list packers: a list of tuples that are used to pack values and transform them
|
||||
:return: packed data string of all values
|
||||
"""
|
||||
|
||||
# iterate through tuples of values to pack
|
||||
logging.debug("packing: %s", packers)
|
||||
data = b""
|
||||
for packer in packers:
|
||||
# check if a transformer was provided for valid values
|
||||
transformer = None
|
||||
if len(packer) == 2:
|
||||
tlv_type, value = packer
|
||||
elif len(packer) == 3:
|
||||
tlv_type, value, transformer = packer
|
||||
else:
|
||||
raise RuntimeError("packer had more than 3 arguments")
|
||||
|
||||
# only pack actual values and avoid packing empty strings
|
||||
# protobuf defaults to empty strings and does no imply a value to set
|
||||
if value is None or (isinstance(value, str) and not value):
|
||||
continue
|
||||
|
||||
# transform values as needed
|
||||
if transformer:
|
||||
value = transformer(value)
|
||||
|
||||
# pack and add to existing data
|
||||
logging.debug("packing: %s - %s type(%s)", tlv_type, value, type(value))
|
||||
data += clazz.pack(tlv_type.value, value)
|
||||
|
||||
return data
|
||||
|
|
@ -4,112 +4,73 @@ Common support for configurable CORE objects.
|
|||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreConfigError
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.location.mobility import WirelessModel
|
||||
|
||||
WirelessModelType = type[WirelessModel]
|
||||
|
||||
_BOOL_OPTIONS: set[str] = {"0", "1"}
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGroup:
|
||||
"""
|
||||
Defines configuration group tabs used for display by ConfigurationOptions.
|
||||
"""
|
||||
|
||||
name: str
|
||||
start: int
|
||||
stop: int
|
||||
def __init__(self, name: str, start: int, stop: int) -> None:
|
||||
"""
|
||||
Creates a ConfigGroup object.
|
||||
|
||||
:param name: configuration group display name
|
||||
:param start: configurations start index for this group
|
||||
:param stop: configurations stop index for this group
|
||||
"""
|
||||
self.name: str = name
|
||||
self.start: int = start
|
||||
self.stop: int = stop
|
||||
|
||||
|
||||
@dataclass
|
||||
class Configuration:
|
||||
"""
|
||||
Represents a configuration option.
|
||||
Represents a configuration options.
|
||||
"""
|
||||
|
||||
id: str
|
||||
type: ConfigDataTypes
|
||||
label: str = None
|
||||
default: str = ""
|
||||
options: list[str] = field(default_factory=list)
|
||||
group: str = "Configuration"
|
||||
def __init__(
|
||||
self,
|
||||
_id: str,
|
||||
_type: ConfigDataTypes,
|
||||
label: str = None,
|
||||
default: str = "",
|
||||
options: List[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a Configuration object.
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.label = self.label if self.label else self.id
|
||||
if self.type == ConfigDataTypes.BOOL:
|
||||
if self.default and self.default not in _BOOL_OPTIONS:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: "
|
||||
f"{self.default}"
|
||||
)
|
||||
elif self.type == ConfigDataTypes.FLOAT:
|
||||
if self.default:
|
||||
try:
|
||||
float(self.default)
|
||||
except ValueError:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} is not a valid float: {self.default}"
|
||||
)
|
||||
elif self.type != ConfigDataTypes.STRING:
|
||||
if self.default:
|
||||
try:
|
||||
int(self.default)
|
||||
except ValueError:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} is not a valid int: {self.default}"
|
||||
)
|
||||
:param _id: unique name for configuration
|
||||
:param _type: configuration data type
|
||||
:param label: configuration label for display
|
||||
:param default: default value for configuration
|
||||
:param options: list options if this is a configuration with a combobox
|
||||
"""
|
||||
self.id: str = _id
|
||||
self.type: ConfigDataTypes = _type
|
||||
self.default: str = default
|
||||
if not options:
|
||||
options = []
|
||||
self.options: List[str] = options
|
||||
if not label:
|
||||
label = _id
|
||||
self.label: str = label
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigBool(Configuration):
|
||||
"""
|
||||
Represents a boolean configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.BOOL
|
||||
value: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigFloat(Configuration):
|
||||
"""
|
||||
Represents a float configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.FLOAT
|
||||
value: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigInt(Configuration):
|
||||
"""
|
||||
Represents an integer configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.INT32
|
||||
value: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigString(Configuration):
|
||||
"""
|
||||
Represents a string configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.STRING
|
||||
value: str = ""
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
|
||||
f"default={self.default}, options={self.options})"
|
||||
)
|
||||
|
||||
|
||||
class ConfigurableOptions:
|
||||
|
|
@ -118,10 +79,11 @@ class ConfigurableOptions:
|
|||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
options: list[Configuration] = []
|
||||
bitmap: Optional[str] = None
|
||||
options: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
"""
|
||||
Provides the configurations for this class.
|
||||
|
||||
|
|
@ -130,7 +92,7 @@ class ConfigurableOptions:
|
|||
return cls.options
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
"""
|
||||
Defines how configurations are grouped.
|
||||
|
||||
|
|
@ -139,7 +101,7 @@ class ConfigurableOptions:
|
|||
return [ConfigGroup("Options", 1, len(cls.configurations()))]
|
||||
|
||||
@classmethod
|
||||
def default_values(cls) -> dict[str, str]:
|
||||
def default_values(cls) -> Dict[str, str]:
|
||||
"""
|
||||
Provides an ordered mapping of configuration keys to default values.
|
||||
|
||||
|
|
@ -165,7 +127,7 @@ class ConfigurableManager:
|
|||
"""
|
||||
self.node_configurations = {}
|
||||
|
||||
def nodes(self) -> list[int]:
|
||||
def nodes(self) -> List[int]:
|
||||
"""
|
||||
Retrieves the ids of all node configurations known by this manager.
|
||||
|
||||
|
|
@ -208,7 +170,7 @@ class ConfigurableManager:
|
|||
|
||||
def set_configs(
|
||||
self,
|
||||
config: dict[str, str],
|
||||
config: Dict[str, str],
|
||||
node_id: int = _default_node,
|
||||
config_type: str = _default_type,
|
||||
) -> None:
|
||||
|
|
@ -220,7 +182,7 @@ class ConfigurableManager:
|
|||
:param config_type: configuration type to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"setting config for node(%s) type(%s): %s", node_id, config_type, config
|
||||
)
|
||||
node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
|
||||
|
|
@ -250,7 +212,7 @@ class ConfigurableManager:
|
|||
|
||||
def get_configs(
|
||||
self, node_id: int = _default_node, config_type: str = _default_type
|
||||
) -> Optional[dict[str, str]]:
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieve configurations for a node and configuration type.
|
||||
|
||||
|
|
@ -264,7 +226,7 @@ class ConfigurableManager:
|
|||
result = node_configs.get(config_type)
|
||||
return result
|
||||
|
||||
def get_all_configs(self, node_id: int = _default_node) -> dict[str, Any]:
|
||||
def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve all current configuration types for a node.
|
||||
|
||||
|
|
@ -284,11 +246,11 @@ class ModelManager(ConfigurableManager):
|
|||
Creates a ModelManager object.
|
||||
"""
|
||||
super().__init__()
|
||||
self.models: dict[str, Any] = {}
|
||||
self.node_models: dict[int, str] = {}
|
||||
self.models: Dict[str, Any] = {}
|
||||
self.node_models: Dict[int, str] = {}
|
||||
|
||||
def set_model_config(
|
||||
self, node_id: int, model_name: str, config: dict[str, str] = None
|
||||
self, node_id: int, model_name: str, config: Dict[str, str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration data for a model.
|
||||
|
|
@ -317,7 +279,7 @@ class ModelManager(ConfigurableManager):
|
|||
# set configuration
|
||||
self.set_configs(model_config, node_id=node_id, config_type=model_name)
|
||||
|
||||
def get_model_config(self, node_id: int, model_name: str) -> dict[str, str]:
|
||||
def get_model_config(self, node_id: int, model_name: str) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieve configuration data for a model.
|
||||
|
||||
|
|
@ -342,7 +304,7 @@ class ModelManager(ConfigurableManager):
|
|||
self,
|
||||
node: Union[WlanNode, EmaneNet],
|
||||
model_class: "WirelessModelType",
|
||||
config: dict[str, str] = None,
|
||||
config: Dict[str, str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set model and model configuration for node.
|
||||
|
|
@ -352,7 +314,7 @@ class ModelManager(ConfigurableManager):
|
|||
:param config: model configuration, None for default configuration
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"setting model(%s) for node(%s): %s", model_class.name, node.id, config
|
||||
)
|
||||
self.set_model_config(node.id, model_class.name, config)
|
||||
|
|
@ -361,7 +323,7 @@ class ModelManager(ConfigurableManager):
|
|||
|
||||
def get_models(
|
||||
self, node: Union[WlanNode, EmaneNet]
|
||||
) -> list[tuple[type, dict[str, str]]]:
|
||||
) -> List[Tuple[Type, Dict[str, str]]]:
|
||||
"""
|
||||
Return a list of model classes and values for a net if one has been
|
||||
configured. This is invoked when exporting a session to XML.
|
||||
|
|
@ -381,5 +343,5 @@ class ModelManager(ConfigurableManager):
|
|||
model_class = self.models[model_name]
|
||||
models.append((model_class, config))
|
||||
|
||||
logger.debug("models for node(%s): %s", node.id, models)
|
||||
logging.debug("models for node(%s): %s", node.id, models)
|
||||
return models
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import abc
|
|||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from mako import exceptions
|
||||
from mako.lookup import TemplateLookup
|
||||
|
|
@ -15,24 +14,9 @@ from core.config import Configuration
|
|||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TEMPLATES_DIR: str = "templates"
|
||||
|
||||
|
||||
def get_template_path(file_path: Path) -> str:
|
||||
"""
|
||||
Utility to convert a given file path to a valid template path format.
|
||||
|
||||
:param file_path: file path to convert
|
||||
:return: template path
|
||||
"""
|
||||
if file_path.is_absolute():
|
||||
template_path = str(file_path.relative_to("/"))
|
||||
else:
|
||||
template_path = str(file_path)
|
||||
return template_path
|
||||
|
||||
|
||||
class ConfigServiceMode(enum.Enum):
|
||||
BLOCKING = 0
|
||||
NON_BLOCKING = 1
|
||||
|
|
@ -43,18 +27,6 @@ class ConfigServiceBootError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class ConfigServiceTemplateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShadowDir:
|
||||
path: str
|
||||
src: Optional[str] = None
|
||||
templates: bool = False
|
||||
has_node_paths: bool = False
|
||||
|
||||
|
||||
class ConfigService(abc.ABC):
|
||||
"""
|
||||
Base class for creating configurable services.
|
||||
|
|
@ -66,9 +38,6 @@ class ConfigService(abc.ABC):
|
|||
# time to wait in seconds for determining if service started successfully
|
||||
validation_timer: int = 5
|
||||
|
||||
# directories to shadow and copy files from
|
||||
shadow_directories: list[ShadowDir] = []
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
Create ConfigService instance.
|
||||
|
|
@ -77,11 +46,11 @@ class ConfigService(abc.ABC):
|
|||
"""
|
||||
self.node: CoreNode = node
|
||||
class_file = inspect.getfile(self.__class__)
|
||||
templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
|
||||
self.config: dict[str, Configuration] = {}
|
||||
self.custom_templates: dict[str, str] = {}
|
||||
self.custom_config: dict[str, str] = {}
|
||||
self.config: Dict[str, Configuration] = {}
|
||||
self.custom_templates: Dict[str, str] = {}
|
||||
self.custom_config: Dict[str, str] = {}
|
||||
configs = self.default_configs[:]
|
||||
self._define_config(configs)
|
||||
|
||||
|
|
@ -108,47 +77,47 @@ class ConfigService(abc.ABC):
|
|||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def directories(self) -> list[str]:
|
||||
def directories(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def files(self) -> list[str]:
|
||||
def files(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def default_configs(self) -> list[Configuration]:
|
||||
def default_configs(self) -> List[Configuration]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def modes(self) -> dict[str, dict[str, str]]:
|
||||
def modes(self) -> Dict[str, Dict[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def executables(self) -> list[str]:
|
||||
def executables(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def dependencies(self) -> list[str]:
|
||||
def dependencies(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> list[str]:
|
||||
def startup(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validate(self) -> list[str]:
|
||||
def validate(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> list[str]:
|
||||
def shutdown(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
|
|
@ -164,8 +133,7 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
:raises ConfigServiceBootError: when there is an error starting service
|
||||
"""
|
||||
logger.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
self.create_shadow_dirs()
|
||||
logging.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
self.create_dirs()
|
||||
self.create_files()
|
||||
wait = self.validation_mode == ConfigServiceMode.BLOCKING
|
||||
|
|
@ -186,7 +154,7 @@ class ConfigService(abc.ABC):
|
|||
try:
|
||||
self.node.cmd(cmd)
|
||||
except CoreCommandError:
|
||||
logger.exception(
|
||||
logging.exception(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failed shutdown: {cmd}"
|
||||
)
|
||||
|
|
@ -200,64 +168,6 @@ class ConfigService(abc.ABC):
|
|||
self.stop()
|
||||
self.start()
|
||||
|
||||
def create_shadow_dirs(self) -> None:
|
||||
"""
|
||||
Creates a shadow of a host system directory recursively
|
||||
to be mapped and live within a node.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory or file
|
||||
"""
|
||||
for shadow_dir in self.shadow_directories:
|
||||
# setup shadow and src paths, using node unique paths when configured
|
||||
shadow_path = Path(shadow_dir.path)
|
||||
if shadow_dir.src is None:
|
||||
src_path = shadow_path
|
||||
else:
|
||||
src_path = Path(shadow_dir.src)
|
||||
if shadow_dir.has_node_paths:
|
||||
src_path = src_path / self.node.name
|
||||
# validate shadow and src paths
|
||||
if not shadow_path.is_absolute():
|
||||
raise CoreError(f"shadow dir({shadow_path}) is not absolute")
|
||||
if not src_path.is_absolute():
|
||||
raise CoreError(f"shadow source dir({src_path}) is not absolute")
|
||||
if not src_path.is_dir():
|
||||
raise CoreError(f"shadow source dir({src_path}) does not exist")
|
||||
# create root of the shadow path within node
|
||||
logger.info(
|
||||
"node(%s) creating shadow directory(%s) src(%s) node paths(%s) "
|
||||
"templates(%s)",
|
||||
self.node.name,
|
||||
shadow_path,
|
||||
src_path,
|
||||
shadow_dir.has_node_paths,
|
||||
shadow_dir.templates,
|
||||
)
|
||||
self.node.create_dir(shadow_path)
|
||||
# find all directories and files to create
|
||||
dir_paths = []
|
||||
file_paths = []
|
||||
for path in src_path.rglob("*"):
|
||||
shadow_src_path = shadow_path / path.relative_to(src_path)
|
||||
if path.is_dir():
|
||||
dir_paths.append(shadow_src_path)
|
||||
else:
|
||||
file_paths.append((path, shadow_src_path))
|
||||
# create all directories within node
|
||||
for path in dir_paths:
|
||||
self.node.create_dir(path)
|
||||
# create all files within node, from templates when configured
|
||||
data = self.data()
|
||||
templates = TemplateLookup(directories=src_path)
|
||||
for path, dst_path in file_paths:
|
||||
if shadow_dir.templates:
|
||||
template = templates.get_template(path.name)
|
||||
rendered = self._render(template, data)
|
||||
self.node.create_file(dst_path, rendered)
|
||||
else:
|
||||
self.node.copy_file(path, dst_path)
|
||||
|
||||
def create_dirs(self) -> None:
|
||||
"""
|
||||
Creates directories for service.
|
||||
|
|
@ -265,18 +175,16 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory
|
||||
"""
|
||||
logger.debug("creating config service directories")
|
||||
for directory in sorted(self.directories):
|
||||
dir_path = Path(directory)
|
||||
for directory in self.directories:
|
||||
try:
|
||||
self.node.create_dir(dir_path)
|
||||
except (CoreCommandError, CoreError):
|
||||
self.node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError):
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failure to create service directory: {directory}"
|
||||
)
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns key/value data, used when rendering file templates.
|
||||
|
||||
|
|
@ -303,7 +211,7 @@ class ConfigService(abc.ABC):
|
|||
"""
|
||||
raise CoreError(f"service({self.name}) unknown template({name})")
|
||||
|
||||
def get_templates(self) -> dict[str, str]:
|
||||
def get_templates(self) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieves mapping of file names to templates for all cases, which
|
||||
includes custom templates, file templates, and text templates.
|
||||
|
|
@ -311,53 +219,19 @@ class ConfigService(abc.ABC):
|
|||
:return: mapping of files to templates
|
||||
"""
|
||||
templates = {}
|
||||
for file in self.files:
|
||||
file_path = Path(file)
|
||||
template_path = get_template_path(file_path)
|
||||
if file in self.custom_templates:
|
||||
template = self.custom_templates[file]
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
template = self.custom_templates[name]
|
||||
template = self.clean_text(template)
|
||||
elif self.templates.has_template(template_path):
|
||||
template = self.templates.get_template(template_path).source
|
||||
elif self.templates.has_template(basename):
|
||||
template = self.templates.get_template(basename).source
|
||||
else:
|
||||
try:
|
||||
template = self.get_text_template(file)
|
||||
except Exception as e:
|
||||
raise ConfigServiceTemplateError(
|
||||
f"node({self.node.name}) service({self.name}) file({file}) "
|
||||
f"failure getting template: {e}"
|
||||
)
|
||||
template = self.get_text_template(name)
|
||||
template = self.clean_text(template)
|
||||
templates[file] = template
|
||||
templates[name] = template
|
||||
return templates
|
||||
|
||||
def get_rendered_templates(self) -> dict[str, str]:
|
||||
templates = {}
|
||||
data = self.data()
|
||||
for file in sorted(self.files):
|
||||
rendered = self._get_rendered_template(file, data)
|
||||
templates[file] = rendered
|
||||
return templates
|
||||
|
||||
def _get_rendered_template(self, file: str, data: dict[str, Any]) -> str:
|
||||
file_path = Path(file)
|
||||
template_path = get_template_path(file_path)
|
||||
if file in self.custom_templates:
|
||||
text = self.custom_templates[file]
|
||||
rendered = self.render_text(text, data)
|
||||
elif self.templates.has_template(template_path):
|
||||
rendered = self.render_template(template_path, data)
|
||||
else:
|
||||
try:
|
||||
text = self.get_text_template(file)
|
||||
except Exception as e:
|
||||
raise ConfigServiceTemplateError(
|
||||
f"node({self.node.name}) service({self.name}) file({file}) "
|
||||
f"failure getting template: {e}"
|
||||
)
|
||||
rendered = self.render_text(text, data)
|
||||
return rendered
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
|
@ -365,13 +239,24 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
"""
|
||||
data = self.data()
|
||||
for file in sorted(self.files):
|
||||
logger.debug(
|
||||
"node(%s) service(%s) template(%s)", self.node.name, self.name, file
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
text = self.custom_templates[name]
|
||||
rendered = self.render_text(text, data)
|
||||
elif self.templates.has_template(basename):
|
||||
rendered = self.render_template(basename, data)
|
||||
else:
|
||||
text = self.get_text_template(name)
|
||||
rendered = self.render_text(text, data)
|
||||
logging.debug(
|
||||
"node(%s) service(%s) template(%s): \n%s",
|
||||
self.node.name,
|
||||
self.name,
|
||||
name,
|
||||
rendered,
|
||||
)
|
||||
rendered = self._get_rendered_template(file, data)
|
||||
file_path = Path(file)
|
||||
self.node.create_file(file_path, rendered)
|
||||
self.node.nodefile(name, rendered)
|
||||
|
||||
def run_startup(self, wait: bool) -> None:
|
||||
"""
|
||||
|
|
@ -415,7 +300,7 @@ class ConfigService(abc.ABC):
|
|||
del cmds[index]
|
||||
index += 1
|
||||
except CoreCommandError:
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"validate command failed: {cmd}"
|
||||
)
|
||||
|
|
@ -426,7 +311,7 @@ class ConfigService(abc.ABC):
|
|||
f"node({self.node.name}) service({self.name}) failed to validate"
|
||||
)
|
||||
|
||||
def _render(self, template: Template, data: dict[str, Any] = None) -> str:
|
||||
def _render(self, template: Template, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders template providing all associated data to template.
|
||||
|
||||
|
|
@ -440,7 +325,7 @@ class ConfigService(abc.ABC):
|
|||
node=self.node, config=self.render_config(), **data
|
||||
)
|
||||
|
||||
def render_text(self, text: str, data: dict[str, Any] = None) -> str:
|
||||
def render_text(self, text: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders text based template providing all associated data to template.
|
||||
|
||||
|
|
@ -458,24 +343,24 @@ class ConfigService(abc.ABC):
|
|||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
)
|
||||
|
||||
def render_template(self, template_path: str, data: dict[str, Any] = None) -> str:
|
||||
def render_template(self, basename: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders file based template providing all associated data to template.
|
||||
|
||||
:param template_path: path of file to render
|
||||
:param basename: base name for file to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
try:
|
||||
template = self.templates.get_template(template_path)
|
||||
template = self.templates.get_template(basename)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) file({template_path})"
|
||||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_template()}"
|
||||
)
|
||||
|
||||
def _define_config(self, configs: list[Configuration]) -> None:
|
||||
def _define_config(self, configs: List[Configuration]) -> None:
|
||||
"""
|
||||
Initializes default configuration data.
|
||||
|
||||
|
|
@ -485,7 +370,7 @@ class ConfigService(abc.ABC):
|
|||
for config in configs:
|
||||
self.config[config.id] = config
|
||||
|
||||
def render_config(self) -> dict[str, str]:
|
||||
def render_config(self) -> Dict[str, str]:
|
||||
"""
|
||||
Returns configuration data key/value pairs for rendering a template.
|
||||
|
||||
|
|
@ -496,7 +381,7 @@ class ConfigService(abc.ABC):
|
|||
else:
|
||||
return {k: v.default for k, v in self.config.items()}
|
||||
|
||||
def set_config(self, data: dict[str, str]) -> None:
|
||||
def set_config(self, data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Set configuration data from key/value pairs.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.configservice.base import ConfigService
|
||||
|
|
@ -12,16 +10,16 @@ class ConfigServiceDependencies:
|
|||
Generates sets of services to start in order of their dependencies.
|
||||
"""
|
||||
|
||||
def __init__(self, services: dict[str, "ConfigService"]) -> None:
|
||||
def __init__(self, services: Dict[str, "ConfigService"]) -> None:
|
||||
"""
|
||||
Create a ConfigServiceDependencies instance.
|
||||
|
||||
:param services: services for determining dependency sets
|
||||
"""
|
||||
# helpers to check validity
|
||||
self.dependents: dict[str, set[str]] = {}
|
||||
self.started: set[str] = set()
|
||||
self.node_services: dict[str, "ConfigService"] = {}
|
||||
self.dependents: Dict[str, Set[str]] = {}
|
||||
self.started: Set[str] = set()
|
||||
self.node_services: Dict[str, "ConfigService"] = {}
|
||||
for service in services.values():
|
||||
self.node_services[service.name] = service
|
||||
for dependency in service.dependencies:
|
||||
|
|
@ -29,11 +27,11 @@ class ConfigServiceDependencies:
|
|||
dependents.add(service.name)
|
||||
|
||||
# used to find paths
|
||||
self.path: list["ConfigService"] = []
|
||||
self.visited: set[str] = set()
|
||||
self.visiting: set[str] = set()
|
||||
self.path: List["ConfigService"] = []
|
||||
self.visited: Set[str] = set()
|
||||
self.visiting: Set[str] = set()
|
||||
|
||||
def startup_paths(self) -> list[list["ConfigService"]]:
|
||||
def startup_paths(self) -> List[List["ConfigService"]]:
|
||||
"""
|
||||
Find startup path sets based on service dependencies.
|
||||
|
||||
|
|
@ -43,7 +41,7 @@ class ConfigServiceDependencies:
|
|||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.started:
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"skipping service that will already be started: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
|
@ -54,8 +52,8 @@ class ConfigServiceDependencies:
|
|||
|
||||
if self.started != set(self.node_services):
|
||||
raise ValueError(
|
||||
f"failure to start all services: {self.started} != "
|
||||
f"{self.node_services.keys()}"
|
||||
"failure to start all services: %s != %s"
|
||||
% (self.started, self.node_services.keys())
|
||||
)
|
||||
|
||||
return paths
|
||||
|
|
@ -70,25 +68,25 @@ class ConfigServiceDependencies:
|
|||
self.visited.clear()
|
||||
self.visiting.clear()
|
||||
|
||||
def _start(self, service: "ConfigService") -> list["ConfigService"]:
|
||||
def _start(self, service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Starts a oath for checking dependencies for a given service.
|
||||
|
||||
:param service: service to check dependencies for
|
||||
:return: list of config services to start in order
|
||||
"""
|
||||
logger.debug("starting service dependency check: %s", service.name)
|
||||
logging.debug("starting service dependency check: %s", service.name)
|
||||
self._reset()
|
||||
return self._visit(service)
|
||||
|
||||
def _visit(self, current_service: "ConfigService") -> list["ConfigService"]:
|
||||
def _visit(self, current_service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Visits a service when discovering dependency chains for service.
|
||||
|
||||
:param current_service: service being visited
|
||||
:return: list of dependent services for a visited service
|
||||
"""
|
||||
logger.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
logging.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
self.visited.add(current_service.name)
|
||||
self.visiting.add(current_service.name)
|
||||
|
||||
|
|
@ -96,14 +94,14 @@ class ConfigServiceDependencies:
|
|||
for service_name in current_service.dependencies:
|
||||
if service_name not in self.node_services:
|
||||
raise ValueError(
|
||||
"required dependency was not included in node "
|
||||
f"services: {service_name}"
|
||||
"required dependency was not included in node services: %s"
|
||||
% service_name
|
||||
)
|
||||
|
||||
if service_name in self.visiting:
|
||||
raise ValueError(
|
||||
f"cyclic dependency at service({current_service.name}): "
|
||||
f"{service_name}"
|
||||
"cyclic dependency at service(%s): %s"
|
||||
% (current_service.name, service_name)
|
||||
)
|
||||
|
||||
if service_name not in self.visited:
|
||||
|
|
@ -111,7 +109,7 @@ class ConfigServiceDependencies:
|
|||
self._visit(service)
|
||||
|
||||
# add service when bottom is found
|
||||
logger.debug("adding service to startup path: %s", current_service.name)
|
||||
logging.debug("adding service to startup path: %s", current_service.name)
|
||||
self.started.add(current_service.name)
|
||||
self.path.append(current_service)
|
||||
self.visiting.remove(current_service.name)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import logging
|
||||
import pathlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from core import configservices, utils
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigServiceManager:
|
||||
"""
|
||||
|
|
@ -19,9 +16,9 @@ class ConfigServiceManager:
|
|||
"""
|
||||
Create a ConfigServiceManager instance.
|
||||
"""
|
||||
self.services: dict[str, type[ConfigService]] = {}
|
||||
self.services: Dict[str, Type[ConfigService]] = {}
|
||||
|
||||
def get_service(self, name: str) -> type[ConfigService]:
|
||||
def get_service(self, name: str) -> Type[ConfigService]:
|
||||
"""
|
||||
Retrieve a service by name.
|
||||
|
||||
|
|
@ -31,10 +28,10 @@ class ConfigServiceManager:
|
|||
"""
|
||||
service_class = self.services.get(name)
|
||||
if service_class is None:
|
||||
raise CoreError(f"service does not exist {name}")
|
||||
raise CoreError(f"service does not exit {name}")
|
||||
return service_class
|
||||
|
||||
def add(self, service: type[ConfigService]) -> None:
|
||||
def add(self, service: Type[ConfigService]) -> None:
|
||||
"""
|
||||
Add service to manager, checking service requirements have been met.
|
||||
|
||||
|
|
@ -43,7 +40,7 @@ class ConfigServiceManager:
|
|||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"loading service: class(%s) name(%s)", service.__class__.__name__, name
|
||||
)
|
||||
|
||||
|
|
@ -58,46 +55,27 @@ class ConfigServiceManager:
|
|||
except CoreError as e:
|
||||
raise CoreError(f"config service({service.name}): {e}")
|
||||
|
||||
# make service available
|
||||
# make service available
|
||||
self.services[name] = service
|
||||
|
||||
def load_locals(self) -> list[str]:
|
||||
def load(self, path: str) -> List[str]:
|
||||
"""
|
||||
Search and add config service from local core module.
|
||||
|
||||
:return: list of errors when loading services
|
||||
"""
|
||||
errors = []
|
||||
for module_info in pkgutil.walk_packages(
|
||||
configservices.__path__, f"{configservices.__name__}."
|
||||
):
|
||||
services = utils.load_module(module_info.name, ConfigService)
|
||||
for service in services:
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
errors.append(service.name)
|
||||
logger.debug("not loading config service(%s): %s", service.name, e)
|
||||
return errors
|
||||
|
||||
def load(self, path: Path) -> list[str]:
|
||||
"""
|
||||
Search path provided for config services and add them for being managed.
|
||||
Search path provided for configurable services and add them for being managed.
|
||||
|
||||
:param path: path to search configurable services
|
||||
:return: list errors when loading services
|
||||
:return: list errors when loading and adding services
|
||||
"""
|
||||
path = pathlib.Path(path)
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
service_errors = []
|
||||
for subdir in subdirs:
|
||||
logger.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(subdir, ConfigService)
|
||||
logging.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(str(subdir), ConfigService)
|
||||
for service in services:
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
service_errors.append(service.name)
|
||||
logger.debug("not loading service(%s): %s", service.name, e)
|
||||
logging.debug("not loading service(%s): %s", service.name, e)
|
||||
return service_errors
|
||||
|
|
|
|||
|
|
@ -1,36 +1,24 @@
|
|||
import abc
|
||||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
GROUP: str = "FRR"
|
||||
FRR_STATE_DIR: str = "/var/run/frr"
|
||||
|
||||
|
||||
def is_wireless(node: NodeBase) -> bool:
|
||||
"""
|
||||
Check if the node is a wireless type node.
|
||||
|
||||
:param node: node to check type for
|
||||
:return: True if wireless type, False otherwise
|
||||
"""
|
||||
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||
|
||||
|
||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate FRR
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if iface.mtu != DEFAULT_MTU:
|
||||
if iface.mtu != 1500:
|
||||
return True
|
||||
if not iface.net:
|
||||
return False
|
||||
|
|
@ -65,47 +53,32 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
return "0.0.0.0"
|
||||
|
||||
|
||||
def rj45_check(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect whether interface is connected an external RJ45
|
||||
link.
|
||||
"""
|
||||
if iface.net:
|
||||
for peer_iface in iface.net.get_ifaces():
|
||||
if peer_iface == iface:
|
||||
continue
|
||||
if isinstance(peer_iface.node, Rj45Node):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FRRZebra(ConfigService):
|
||||
name: str = "FRRzebra"
|
||||
group: str = GROUP
|
||||
directories: list[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files: list[str] = [
|
||||
directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files: List[str] = [
|
||||
"/usr/local/etc/frr/frr.conf",
|
||||
"frrboot.sh",
|
||||
"/usr/local/etc/frr/vtysh.conf",
|
||||
"/usr/local/etc/frr/daemons",
|
||||
]
|
||||
executables: list[str] = ["zebra"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash frrboot.sh zebra"]
|
||||
validate: list[str] = ["pidof zebra"]
|
||||
shutdown: list[str] = ["killall zebra"]
|
||||
executables: List[str] = ["zebra"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh frrboot.sh zebra"]
|
||||
validate: List[str] = ["pidof zebra"]
|
||||
shutdown: List[str] = ["killall zebra"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
frr_conf = self.files[0]
|
||||
frr_bin_search = self.node.session.options.get(
|
||||
frr_bin_search = self.node.session.options.get_config(
|
||||
"frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr"
|
||||
).strip('"')
|
||||
frr_sbin_search = self.node.session.options.get(
|
||||
"frr_sbin_search",
|
||||
default="/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr",
|
||||
frr_sbin_search = self.node.session.options.get_config(
|
||||
"frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr"
|
||||
).strip('"')
|
||||
|
||||
services = []
|
||||
|
|
@ -130,7 +103,8 @@ class FRRZebra(ConfigService):
|
|||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
|
|
@ -146,16 +120,16 @@ class FRRZebra(ConfigService):
|
|||
|
||||
class FrrService(abc.ABC):
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = []
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = ["FRRzebra"]
|
||||
startup: list[str] = []
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = []
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = ["FRRzebra"]
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
|
|
@ -176,8 +150,8 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRROSPFv2"
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
shutdown: List[str] = ["killall ospfd"]
|
||||
validate: List[str] = ["pidof ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
@ -185,7 +159,7 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
addresses = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4))
|
||||
addresses.append(str(ip4.ip))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
|
@ -193,31 +167,15 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
ospf opaque-lsa
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
has_mtu = has_mtu_mismatch(iface)
|
||||
has_rj45 = rj45_check(iface)
|
||||
is_ptp = isinstance(iface.net, PtpNet)
|
||||
data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45)
|
||||
text = """
|
||||
% if has_mtu:
|
||||
ip ospf mtu-ignore
|
||||
% endif
|
||||
% if has_rj45:
|
||||
<% return STOP_RENDERING %>
|
||||
% endif
|
||||
% if is_ptp:
|
||||
ip ospf network point-to-point
|
||||
% endif
|
||||
ip ospf hello-interval 2
|
||||
ip ospf dead-interval 6
|
||||
ip ospf retransmit-interval 5
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
if has_mtu_mismatch(iface):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class FRROspfv3(FrrService, ConfigService):
|
||||
|
|
@ -228,8 +186,8 @@ class FRROspfv3(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRROSPFv3"
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
shutdown: List[str] = ["killall ospf6d"]
|
||||
validate: List[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
|
|
@ -265,8 +223,8 @@ class FRRBgp(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRBGP"
|
||||
shutdown: list[str] = ["killall bgpd"]
|
||||
validate: list[str] = ["pidof bgpd"]
|
||||
shutdown: List[str] = ["killall bgpd"]
|
||||
validate: List[str] = ["pidof bgpd"]
|
||||
custom_needed: bool = True
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
|
@ -295,8 +253,8 @@ class FRRRip(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRRIP"
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
shutdown: List[str] = ["killall ripd"]
|
||||
validate: List[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
@ -320,8 +278,8 @@ class FRRRipng(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRRIPNG"
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
shutdown: List[str] = ["killall ripngd"]
|
||||
validate: List[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
@ -346,8 +304,8 @@ class FRRBabel(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRBabel"
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
shutdown: List[str] = ["killall babeld"]
|
||||
validate: List[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
@ -367,7 +325,7 @@ class FRRBabel(FrrService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if is_wireless(iface.net):
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
|
@ -386,8 +344,8 @@ class FRRpimd(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRpimd"
|
||||
shutdown: list[str] = ["killall pimd"]
|
||||
validate: list[str] = ["pidof pimd"]
|
||||
shutdown: List[str] = ["killall pimd"]
|
||||
validate: List[str] = ["pidof pimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
|
|||
|
|
@ -48,10 +48,6 @@ bootdaemon()
|
|||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
if [ "$1" = "ospfd" ]; then
|
||||
flags="$flags --apiserver"
|
||||
fi
|
||||
|
||||
#force FRR to use CORE generated conf file
|
||||
flags="$flags -d -f $FRR_CONF"
|
||||
$FRR_SBIN_DIR/$1 $flags
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from core import utils
|
||||
from core.config import Configuration
|
||||
|
|
@ -10,18 +10,18 @@ GROUP: str = "ProtoSvc"
|
|||
class MgenSinkService(ConfigService):
|
||||
name: str = "MGEN_Sink"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["mgensink.sh", "sink.mgen"]
|
||||
executables: list[str] = ["mgen"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash mgensink.sh"]
|
||||
validate: list[str] = ["pidof mgen"]
|
||||
shutdown: list[str] = ["killall mgen"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["mgensink.sh", "sink.mgen"]
|
||||
executables: List[str] = ["mgen"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh mgensink.sh"]
|
||||
validate: List[str] = ["pidof mgen"]
|
||||
shutdown: List[str] = ["killall mgen"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces():
|
||||
name = utils.sysctl_devname(iface.name)
|
||||
|
|
@ -32,18 +32,18 @@ class MgenSinkService(ConfigService):
|
|||
class NrlNhdp(ConfigService):
|
||||
name: str = "NHDP"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlnhdp.sh"]
|
||||
executables: list[str] = ["nrlnhdp"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlnhdp.sh"]
|
||||
validate: list[str] = ["pidof nrlnhdp"]
|
||||
shutdown: list[str] = ["killall nrlnhdp"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlnhdp.sh"]
|
||||
executables: List[str] = ["nrlnhdp"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh nrlnhdp.sh"]
|
||||
validate: List[str] = ["pidof nrlnhdp"]
|
||||
shutdown: List[str] = ["killall nrlnhdp"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
|
@ -54,18 +54,19 @@ class NrlNhdp(ConfigService):
|
|||
class NrlSmf(ConfigService):
|
||||
name: str = "SMF"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["startsmf.sh"]
|
||||
executables: list[str] = ["nrlsmf", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startsmf.sh"]
|
||||
validate: list[str] = ["pidof nrlsmf"]
|
||||
shutdown: list[str] = ["killall nrlsmf"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startsmf.sh"]
|
||||
executables: List[str] = ["nrlsmf", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh startsmf.sh"]
|
||||
validate: List[str] = ["pidof nrlsmf"]
|
||||
shutdown: List[str] = ["killall nrlsmf"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_arouted = "arouted" in self.node.config_services
|
||||
has_nhdp = "NHDP" in self.node.config_services
|
||||
has_olsr = "OLSR" in self.node.config_services
|
||||
ifnames = []
|
||||
|
|
@ -77,25 +78,29 @@ class NrlSmf(ConfigService):
|
|||
ip4_prefix = f"{ip4.ip}/{24}"
|
||||
break
|
||||
return dict(
|
||||
has_nhdp=has_nhdp, has_olsr=has_olsr, ifnames=ifnames, ip4_prefix=ip4_prefix
|
||||
has_arouted=has_arouted,
|
||||
has_nhdp=has_nhdp,
|
||||
has_olsr=has_olsr,
|
||||
ifnames=ifnames,
|
||||
ip4_prefix=ip4_prefix,
|
||||
)
|
||||
|
||||
|
||||
class NrlOlsr(ConfigService):
|
||||
name: str = "OLSR"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlolsrd.sh"]
|
||||
executables: list[str] = ["nrlolsrd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlolsrd.sh"]
|
||||
validate: list[str] = ["pidof nrlolsrd"]
|
||||
shutdown: list[str] = ["killall nrlolsrd"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlolsrd.sh"]
|
||||
executables: List[str] = ["nrlolsrd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh nrlolsrd.sh"]
|
||||
validate: List[str] = ["pidof nrlolsrd"]
|
||||
shutdown: List[str] = ["killall nrlolsrd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
has_zebra = "zebra" in self.node.config_services
|
||||
ifname = None
|
||||
|
|
@ -108,18 +113,18 @@ class NrlOlsr(ConfigService):
|
|||
class NrlOlsrv2(ConfigService):
|
||||
name: str = "OLSRv2"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlolsrv2.sh"]
|
||||
executables: list[str] = ["nrlolsrv2"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlolsrv2.sh"]
|
||||
validate: list[str] = ["pidof nrlolsrv2"]
|
||||
shutdown: list[str] = ["killall nrlolsrv2"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlolsrv2.sh"]
|
||||
executables: List[str] = ["nrlolsrv2"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh nrlolsrv2.sh"]
|
||||
validate: List[str] = ["pidof nrlolsrv2"]
|
||||
shutdown: List[str] = ["killall nrlolsrv2"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
|
@ -130,18 +135,18 @@ class NrlOlsrv2(ConfigService):
|
|||
class OlsrOrg(ConfigService):
|
||||
name: str = "OLSRORG"
|
||||
group: str = GROUP
|
||||
directories: list[str] = ["/etc/olsrd"]
|
||||
files: list[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables: list[str] = ["olsrd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash olsrd.sh"]
|
||||
validate: list[str] = ["pidof olsrd"]
|
||||
shutdown: list[str] = ["killall olsrd"]
|
||||
directories: List[str] = ["/etc/olsrd"]
|
||||
files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables: List[str] = ["olsrd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh olsrd.sh"]
|
||||
validate: List[str] = ["pidof olsrd"]
|
||||
shutdown: List[str] = ["killall olsrd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
|
@ -152,13 +157,37 @@ class OlsrOrg(ConfigService):
|
|||
class MgenActor(ConfigService):
|
||||
name: str = "MgenActor"
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["start_mgen_actor.sh"]
|
||||
executables: list[str] = ["mgen"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash start_mgen_actor.sh"]
|
||||
validate: list[str] = ["pidof mgen"]
|
||||
shutdown: list[str] = ["killall mgen"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["start_mgen_actor.sh"]
|
||||
executables: List[str] = ["mgen"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh start_mgen_actor.sh"]
|
||||
validate: List[str] = ["pidof mgen"]
|
||||
shutdown: List[str] = ["killall mgen"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class Arouted(ConfigService):
|
||||
name: str = "arouted"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startarouted.sh"]
|
||||
executables: List[str] = ["arouted"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh startarouted.sh"]
|
||||
validate: List[str] = ["pidof arouted"]
|
||||
shutdown: List[str] = ["pkill arouted"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ip4_prefix = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
ip4_prefix = f"{ip4.ip}/{24}"
|
||||
break
|
||||
return dict(ip4_prefix=ip4_prefix)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
for f in "/tmp/${node.name}_smf"; do
|
||||
count=1
|
||||
until [ -e "$f" ]; do
|
||||
if [ $count -eq 10 ]; then
|
||||
echo "ERROR: nrlmsf pipe not found: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.1
|
||||
count=$(($count + 1))
|
||||
done
|
||||
done
|
||||
|
||||
ip route add ${ip4_prefix} dev lo
|
||||
arouted instance ${node.name}_smf tap ${node.name}_tap stability 10 2>&1 > /var/log/arouted.log &
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
<%
|
||||
ifaces = ",".join(ifnames)
|
||||
arouted = ""
|
||||
if has_arouted:
|
||||
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
||||
if has_nhdp:
|
||||
flood = "ecds"
|
||||
elif has_olsr:
|
||||
|
|
@ -9,4 +12,4 @@
|
|||
%>
|
||||
#!/bin/sh
|
||||
# auto-generated by NrlSmf service
|
||||
nrlsmf instance ${node.name}_smf ${flood} ${ifaces} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
|
|
|
|||
|
|
@ -1,38 +1,25 @@
|
|||
import abc
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
GROUP: str = "Quagga"
|
||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||
|
||||
|
||||
def is_wireless(node: NodeBase) -> bool:
|
||||
"""
|
||||
Check if the node is a wireless type node.
|
||||
|
||||
:param node: node to check type for
|
||||
:return: True if wireless type, False otherwise
|
||||
"""
|
||||
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||
|
||||
|
||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if iface.mtu != DEFAULT_MTU:
|
||||
if iface.mtu != 1500:
|
||||
return True
|
||||
if not iface.net:
|
||||
return False
|
||||
|
|
@ -67,43 +54,29 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
return "0.0.0.0"
|
||||
|
||||
|
||||
def rj45_check(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect whether interface is connected an external RJ45
|
||||
link.
|
||||
"""
|
||||
if iface.net:
|
||||
for peer_iface in iface.net.get_ifaces():
|
||||
if peer_iface == iface:
|
||||
continue
|
||||
if isinstance(peer_iface.node, Rj45Node):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Zebra(ConfigService):
|
||||
name: str = "zebra"
|
||||
group: str = GROUP
|
||||
directories: list[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files: list[str] = [
|
||||
directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files: List[str] = [
|
||||
"/usr/local/etc/quagga/Quagga.conf",
|
||||
"quaggaboot.sh",
|
||||
"/usr/local/etc/quagga/vtysh.conf",
|
||||
]
|
||||
executables: list[str] = ["zebra"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash quaggaboot.sh zebra"]
|
||||
validate: list[str] = ["pidof zebra"]
|
||||
shutdown: list[str] = ["killall zebra"]
|
||||
executables: List[str] = ["zebra"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh quaggaboot.sh zebra"]
|
||||
validate: List[str] = ["pidof zebra"]
|
||||
shutdown: List[str] = ["killall zebra"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get(
|
||||
def data(self) -> Dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get_config(
|
||||
"quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_sbin_search = self.node.session.options.get(
|
||||
quagga_sbin_search = self.node.session.options.get_config(
|
||||
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_state_dir = QUAGGA_STATE_DIR
|
||||
|
|
@ -128,16 +101,11 @@ class Zebra(ConfigService):
|
|||
ip4s = []
|
||||
ip6s = []
|
||||
for ip4 in iface.ip4s:
|
||||
ip4s.append(str(ip4))
|
||||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6))
|
||||
configs = []
|
||||
if not iface.control:
|
||||
for service in services:
|
||||
config = service.quagga_iface_config(iface)
|
||||
if config:
|
||||
configs.append(config.split("\n"))
|
||||
ifaces.append((iface, ip4s, ip6s, configs))
|
||||
ip6s.append(str(ip6.ip))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
quagga_bin_search=quagga_bin_search,
|
||||
|
|
@ -153,16 +121,16 @@ class Zebra(ConfigService):
|
|||
|
||||
class QuaggaService(abc.ABC):
|
||||
group: str = GROUP
|
||||
directories: list[str] = []
|
||||
files: list[str] = []
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = ["zebra"]
|
||||
startup: list[str] = []
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = []
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = ["zebra"]
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
|
|
@ -183,37 +151,22 @@ class Ospfv2(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "OSPFv2"
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
validate: List[str] = ["pidof ospfd"]
|
||||
shutdown: List[str] = ["killall ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
has_mtu = has_mtu_mismatch(iface)
|
||||
has_rj45 = rj45_check(iface)
|
||||
is_ptp = isinstance(iface.net, PtpNet)
|
||||
data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45)
|
||||
text = """
|
||||
% if has_mtu:
|
||||
ip ospf mtu-ignore
|
||||
% endif
|
||||
% if has_rj45:
|
||||
<% return STOP_RENDERING %>
|
||||
% endif
|
||||
% if is_ptp:
|
||||
ip ospf network point-to-point
|
||||
% endif
|
||||
ip ospf hello-interval 2
|
||||
ip ospf dead-interval 6
|
||||
ip ospf retransmit-interval 5
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
if has_mtu_mismatch(iface):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4))
|
||||
addresses.append(str(ip4.ip))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
|
@ -234,8 +187,8 @@ class Ospfv3(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "OSPFv3"
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
shutdown: List[str] = ["killall ospf6d"]
|
||||
validate: List[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
|
|
@ -274,9 +227,15 @@ class Ospfv3mdr(Ospfv3):
|
|||
|
||||
name: str = "OSPFv3MDR"
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
for iface in self.node.get_ifaces():
|
||||
is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
|
||||
logging.info("MDR wireless: %s", is_wireless)
|
||||
return dict()
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
config = super().quagga_iface_config(iface)
|
||||
if is_wireless(iface.net):
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
|
|
@ -300,12 +259,15 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "BGP"
|
||||
shutdown: list[str] = ["killall bgpd"]
|
||||
validate: list[str] = ["pidof bgpd"]
|
||||
shutdown: List[str] = ["killall bgpd"]
|
||||
validate: List[str] = ["pidof bgpd"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
return ""
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
|
|
@ -319,9 +281,6 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
|
|
@ -329,8 +288,8 @@ class Rip(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "RIP"
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
shutdown: List[str] = ["killall ripd"]
|
||||
validate: List[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
|
@ -354,8 +313,8 @@ class Ripng(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "RIPNG"
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
shutdown: List[str] = ["killall ripngd"]
|
||||
validate: List[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
|
@ -380,8 +339,8 @@ class Babel(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "Babel"
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
shutdown: List[str] = ["killall babeld"]
|
||||
validate: List[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
|
@ -401,7 +360,7 @@ class Babel(QuaggaService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
if is_wireless(iface.net):
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
|
@ -420,8 +379,8 @@ class Xpimd(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "Xpimd"
|
||||
shutdown: list[str] = ["killall xpimd"]
|
||||
validate: list[str] = ["pidof xpimd"]
|
||||
shutdown: List[str] = ["killall xpimd"]
|
||||
validate: List[str] = ["pidof xpimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
% for iface, ip4s, ip6s, configs in ifaces:
|
||||
% for iface, ip4s, ip6s, is_control in ifaces:
|
||||
interface ${iface.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
|
|
@ -10,11 +10,13 @@ interface ${iface.name}
|
|||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% for config in configs:
|
||||
% for line in config:
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.quagga_iface_config(iface).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from core.config import ConfigString, Configuration
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
GROUP_NAME: str = "Security"
|
||||
|
||||
|
|
@ -9,41 +10,71 @@ GROUP_NAME: str = "Security"
|
|||
class VpnClient(ConfigService):
|
||||
name: str = "VPNClient"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["vpnclient.sh"]
|
||||
executables: list[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash vpnclient.sh"]
|
||||
validate: list[str] = ["pidof openvpn"]
|
||||
shutdown: list[str] = ["killall openvpn"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["vpnclient.sh"]
|
||||
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh vpnclient.sh"]
|
||||
validate: List[str] = ["pidof openvpn"]
|
||||
shutdown: List[str] = ["killall openvpn"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = [
|
||||
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
|
||||
ConfigString(id="keyname", label="Key Name", default="client1"),
|
||||
ConfigString(id="server", label="Server", default="10.0.2.10"),
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="client1",
|
||||
),
|
||||
Configuration(
|
||||
_id="server",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Server",
|
||||
default="10.0.2.10",
|
||||
),
|
||||
]
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class VpnServer(ConfigService):
|
||||
name: str = "VPNServer"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["vpnserver.sh"]
|
||||
executables: list[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash vpnserver.sh"]
|
||||
validate: list[str] = ["pidof openvpn"]
|
||||
shutdown: list[str] = ["killall openvpn"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["vpnserver.sh"]
|
||||
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh vpnserver.sh"]
|
||||
validate: List[str] = ["pidof openvpn"]
|
||||
shutdown: List[str] = ["killall openvpn"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = [
|
||||
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
|
||||
ConfigString(id="keyname", label="Key Name", default="server"),
|
||||
ConfigString(id="subnet", label="Subnet", default="10.0.200.0"),
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="server",
|
||||
),
|
||||
Configuration(
|
||||
_id="subnet",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Subnet",
|
||||
default="10.0.200.0",
|
||||
),
|
||||
]
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
address = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
|
|
@ -56,48 +87,48 @@ class VpnServer(ConfigService):
|
|||
class IPsec(ConfigService):
|
||||
name: str = "IPsec"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["ipsec.sh"]
|
||||
executables: list[str] = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash ipsec.sh"]
|
||||
validate: list[str] = ["pidof racoon"]
|
||||
shutdown: list[str] = ["killall racoon"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["ipsec.sh"]
|
||||
executables: List[str] = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh ipsec.sh"]
|
||||
validate: List[str] = ["pidof racoon"]
|
||||
shutdown: List[str] = ["killall racoon"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class Firewall(ConfigService):
|
||||
name: str = "Firewall"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["firewall.sh"]
|
||||
executables: list[str] = ["iptables"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash firewall.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["firewall.sh"]
|
||||
executables: List[str] = ["iptables"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh firewall.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class Nat(ConfigService):
|
||||
name: str = "NAT"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nat.sh"]
|
||||
executables: list[str] = ["iptables"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nat.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nat.sh"]
|
||||
executables: List[str] = ["iptables"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh nat.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
|
|
|
|||
49
daemon/core/configservices/simpleservice.py
Normal file
49
daemon/core/configservices/simpleservice.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from typing import Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class SimpleService(ConfigService):
|
||||
name: str = "Simple"
|
||||
group: str = "SimpleGroup"
|
||||
directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
|
||||
files: List[str] = ["test1.sh", "test2.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||
Configuration(
|
||||
_id="value3",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Multiple Choice",
|
||||
options=["value1", "value2", "value3"],
|
||||
),
|
||||
]
|
||||
modes: Dict[str, Dict[str, str]] = {
|
||||
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
||||
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
||||
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
||||
}
|
||||
|
||||
def get_text_template(self, name: str) -> str:
|
||||
if name == "test1.sh":
|
||||
return """
|
||||
# sample script 1
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello
|
||||
"""
|
||||
elif name == "test2.sh":
|
||||
return """
|
||||
# sample script 2
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello2
|
||||
"""
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import netaddr
|
||||
|
||||
|
|
@ -12,18 +12,18 @@ GROUP_NAME = "Utility"
|
|||
class DefaultRouteService(ConfigService):
|
||||
name: str = "DefaultRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["defaultroute.sh"]
|
||||
executables: list[str] = ["ip"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash defaultroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["defaultroute.sh"]
|
||||
executables: List[str] = ["ip"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh defaultroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
# only add default routes for linked routing nodes
|
||||
routes = []
|
||||
ifaces = self.node.get_ifaces()
|
||||
|
|
@ -40,18 +40,18 @@ class DefaultRouteService(ConfigService):
|
|||
class DefaultMulticastRouteService(ConfigService):
|
||||
name: str = "DefaultMulticastRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["defaultmroute.sh"]
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash defaultmroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["defaultmroute.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh defaultmroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifname = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifname = iface.name
|
||||
|
|
@ -62,18 +62,18 @@ class DefaultMulticastRouteService(ConfigService):
|
|||
class StaticRouteService(ConfigService):
|
||||
name: str = "StaticRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["staticroute.sh"]
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash staticroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["staticroute.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh staticroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
routes = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip in iface.ips():
|
||||
|
|
@ -90,18 +90,18 @@ class StaticRouteService(ConfigService):
|
|||
class IpForwardService(ConfigService):
|
||||
name: str = "IPForward"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["ipforward.sh"]
|
||||
executables: list[str] = ["sysctl"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash ipforward.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["ipforward.sh"]
|
||||
executables: List[str] = ["sysctl"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh ipforward.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
devnames = []
|
||||
for iface in self.node.get_ifaces():
|
||||
devname = utils.sysctl_devname(iface.name)
|
||||
|
|
@ -112,18 +112,18 @@ class IpForwardService(ConfigService):
|
|||
class SshService(ConfigService):
|
||||
name: str = "SSH"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = ["/etc/ssh", "/var/run/sshd"]
|
||||
files: list[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables: list[str] = ["sshd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startsshd.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = ["killall sshd"]
|
||||
directories: List[str] = ["/etc/ssh", "/var/run/sshd"]
|
||||
files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables: List[str] = ["sshd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh startsshd.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = ["killall sshd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
return dict(
|
||||
sshcfgdir=self.directories[0],
|
||||
sshstatedir=self.directories[1],
|
||||
|
|
@ -134,46 +134,44 @@ class SshService(ConfigService):
|
|||
class DhcpService(ConfigService):
|
||||
name: str = "DHCP"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files: list[str] = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables: list[str] = ["dhcpd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate: list[str] = ["pidof dhcpd"]
|
||||
shutdown: list[str] = ["killall dhcpd"]
|
||||
directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files: List[str] = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables: List[str] = ["dhcpd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate: List[str] = ["pidof dhcpd"]
|
||||
shutdown: List[str] = ["killall dhcpd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
subnets = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
if ip4.size == 1:
|
||||
continue
|
||||
# divide the address space in half
|
||||
index = (ip4.size - 2) / 2
|
||||
rangelow = ip4[index]
|
||||
rangehigh = ip4[-2]
|
||||
subnets.append((ip4.cidr.ip, ip4.netmask, rangelow, rangehigh, ip4.ip))
|
||||
subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip)))
|
||||
return dict(subnets=subnets)
|
||||
|
||||
|
||||
class DhcpClientService(ConfigService):
|
||||
name: str = "DHCPClient"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["startdhcpclient.sh"]
|
||||
executables: list[str] = ["dhclient"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startdhcpclient.sh"]
|
||||
validate: list[str] = ["pidof dhclient"]
|
||||
shutdown: list[str] = ["killall dhclient"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startdhcpclient.sh"]
|
||||
executables: List[str] = ["dhclient"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh startdhcpclient.sh"]
|
||||
validate: List[str] = ["pidof dhclient"]
|
||||
shutdown: List[str] = ["killall dhclient"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
|
|
@ -183,56 +181,56 @@ class DhcpClientService(ConfigService):
|
|||
class FtpService(ConfigService):
|
||||
name: str = "FTP"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files: list[str] = ["vsftpd.conf"]
|
||||
executables: list[str] = ["vsftpd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["vsftpd ./vsftpd.conf"]
|
||||
validate: list[str] = ["pidof vsftpd"]
|
||||
shutdown: list[str] = ["killall vsftpd"]
|
||||
directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files: List[str] = ["vsftpd.conf"]
|
||||
executables: List[str] = ["vsftpd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["vsftpd ./vsftpd.conf"]
|
||||
validate: List[str] = ["pidof vsftpd"]
|
||||
shutdown: List[str] = ["killall vsftpd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class PcapService(ConfigService):
|
||||
name: str = "pcap"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["pcap.sh"]
|
||||
executables: list[str] = ["tcpdump"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash pcap.sh start"]
|
||||
validate: list[str] = ["pidof tcpdump"]
|
||||
shutdown: list[str] = ["bash pcap.sh stop"]
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["pcap.sh"]
|
||||
executables: List[str] = ["tcpdump"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh pcap.sh start"]
|
||||
validate: List[str] = ["pidof tcpdump"]
|
||||
shutdown: List[str] = ["sh pcap.sh stop"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(ifnames=ifnames)
|
||||
return dict()
|
||||
|
||||
|
||||
class RadvdService(ConfigService):
|
||||
name: str = "radvd"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = ["/etc/radvd", "/var/run/radvd"]
|
||||
files: list[str] = ["/etc/radvd/radvd.conf"]
|
||||
executables: list[str] = ["radvd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = [
|
||||
directories: List[str] = ["/etc/radvd"]
|
||||
files: List[str] = ["/etc/radvd/radvd.conf"]
|
||||
executables: List[str] = ["radvd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = [
|
||||
"radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
|
||||
]
|
||||
validate: list[str] = ["pidof radvd"]
|
||||
shutdown: list[str] = ["pkill radvd"]
|
||||
validate: List[str] = ["pidof radvd"]
|
||||
shutdown: List[str] = ["pkill radvd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
prefixes = []
|
||||
|
|
@ -247,22 +245,22 @@ class RadvdService(ConfigService):
|
|||
class AtdService(ConfigService):
|
||||
name: str = "atd"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files: list[str] = ["startatd.sh"]
|
||||
executables: list[str] = ["atd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startatd.sh"]
|
||||
validate: list[str] = ["pidof atd"]
|
||||
shutdown: list[str] = ["pkill atd"]
|
||||
directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files: List[str] = ["startatd.sh"]
|
||||
executables: List[str] = ["atd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["sh startatd.sh"]
|
||||
validate: List[str] = ["pidof atd"]
|
||||
shutdown: List[str] = ["pkill atd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class HttpService(ConfigService):
|
||||
name: str = "HTTP"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = [
|
||||
directories: List[str] = [
|
||||
"/etc/apache2",
|
||||
"/var/run/apache2",
|
||||
"/var/log/apache2",
|
||||
|
|
@ -270,21 +268,21 @@ class HttpService(ConfigService):
|
|||
"/var/lock/apache2",
|
||||
"/var/www",
|
||||
]
|
||||
files: list[str] = [
|
||||
files: List[str] = [
|
||||
"/etc/apache2/apache2.conf",
|
||||
"/etc/apache2/envvars",
|
||||
"/var/www/index.html",
|
||||
]
|
||||
executables: list[str] = ["apache2ctl"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate: list[str] = ["pidof apache2"]
|
||||
shutdown: list[str] = ["apache2ctl stop"]
|
||||
executables: List[str] = ["apache2ctl"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate: List[str] = ["pidof apache2"]
|
||||
shutdown: List[str] = ["apache2ctl stop"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifaces.append(iface)
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@ sysctl -w net.ipv4.conf.default.rp_filter=0
|
|||
sysctl -w net.ipv4.conf.${devname}.forwarding=1
|
||||
sysctl -w net.ipv4.conf.${devname}.send_redirects=0
|
||||
sysctl -w net.ipv4.conf.${devname}.rp_filter=0
|
||||
sysctl -w net.ipv6.conf.${devname}.forwarding=1
|
||||
% endfor
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# (-s snap length, -C limit pcap file length, -n disable name resolution)
|
||||
if [ "x$1" = "xstart" ]; then
|
||||
% for ifname in ifnames:
|
||||
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} > /dev/null 2>&1 &
|
||||
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} < /dev/null &
|
||||
% endfor
|
||||
elif [ "x$1" = "xstop" ]; then
|
||||
mkdir -p $SESSION_DIR/pcap
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# auto-generated by RADVD service (utility.py)
|
||||
% for ifname, prefixes in ifaces:
|
||||
% for ifname, prefixes in values:
|
||||
interface ${ifname}
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from pathlib import Path
|
||||
|
||||
COREDPY_VERSION: str = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@")
|
||||
CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@")
|
||||
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
||||
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import List, Set
|
||||
|
||||
from core.config import ConfigBool, Configuration
|
||||
from core.config import Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class EmaneBypassModel(emanemodel.EmaneModel):
|
||||
name: str = "emane_bypass"
|
||||
|
||||
# values to ignore, when writing xml files
|
||||
config_ignore: set[str] = {"none"}
|
||||
config_ignore: Set[str] = {"none"}
|
||||
|
||||
# mac definitions
|
||||
mac_library: str = "bypassmaclayer"
|
||||
mac_config: list[Configuration] = [
|
||||
ConfigBool(
|
||||
id="none",
|
||||
mac_config: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
|
|
@ -25,8 +27,9 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
|
||||
# phy definitions
|
||||
phy_library: str = "bypassphylayer"
|
||||
phy_config: list[Configuration] = []
|
||||
phy_config: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
# ignore default logic
|
||||
pass
|
||||
|
|
@ -3,7 +3,8 @@ commeffect.py: EMANE CommEffect model for CORE
|
|||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
|
@ -13,8 +14,6 @@ from core.emulator.data import LinkOptions
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
|
|
@ -22,7 +21,7 @@ except ImportError:
|
|||
from emanesh.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
CommEffectEvent = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def convert_none(x: float) -> int:
|
||||
|
|
@ -41,36 +40,27 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
name: str = "emane_commeffect"
|
||||
shim_library: str = "commeffectshim"
|
||||
shim_xml: str = "commeffectshim.xml"
|
||||
shim_defaults: dict[str, str] = {}
|
||||
config_shim: list[Configuration] = []
|
||||
shim_defaults: Dict[str, str] = {}
|
||||
config_shim: List[Configuration] = []
|
||||
|
||||
# comm effect does not need the default phy and external configurations
|
||||
phy_config: list[Configuration] = []
|
||||
external_config: list[Configuration] = []
|
||||
phy_config: List[Configuration] = []
|
||||
external_config: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
||||
shim_xml_path = emane_prefix / "share/emane/manifest" / cls.shim_xml
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml)
|
||||
cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
return cls.platform_config + cls.config_shim
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
return cls.config_shim
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
platform_len = len(cls.platform_config)
|
||||
return [
|
||||
ConfigGroup("Platform Parameters", 1, platform_len),
|
||||
ConfigGroup(
|
||||
"CommEffect SHIM Parameters",
|
||||
platform_len + 1,
|
||||
len(cls.configurations()),
|
||||
),
|
||||
]
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
|
||||
|
||||
def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
|
||||
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||
"""
|
||||
Build the necessary nem and commeffect XMLs in the given path.
|
||||
If an individual NEM has a nonstandard config, we need to build
|
||||
|
|
@ -90,7 +80,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
nem_name = emanexml.nem_file_name(iface)
|
||||
shim_name = emanexml.shim_file_name(iface)
|
||||
etree.SubElement(nem_element, "shim", definition=shim_name)
|
||||
emanexml.create_node_file(iface.node, nem_element, "nem", nem_name)
|
||||
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
|
||||
|
||||
# create and write shim document
|
||||
shim_element = etree.Element(
|
||||
|
|
@ -109,7 +99,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
ff = config["filterfile"]
|
||||
if ff.strip() != "":
|
||||
emanexml.add_param(shim_element, "filterfile", ff)
|
||||
emanexml.create_node_file(iface.node, shim_element, "shim", shim_name)
|
||||
emanexml.create_iface_file(iface, shim_element, "shim", shim_name)
|
||||
|
||||
# create transport xml
|
||||
emanexml.create_transport_xml(iface, config)
|
||||
|
|
@ -121,15 +111,21 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
Generate CommEffect events when a Link Message is received having
|
||||
link parameters.
|
||||
"""
|
||||
if iface is None or iface2 is None:
|
||||
logger.warning("%s: missing NEM information", self.name)
|
||||
service = self.session.emane.service
|
||||
if service is None:
|
||||
logging.warning("%s: EMANE event service unavailable", self.name)
|
||||
return
|
||||
|
||||
if iface is None or iface2 is None:
|
||||
logging.warning("%s: missing NEM information", self.name)
|
||||
return
|
||||
|
||||
# TODO: batch these into multiple events per transmission
|
||||
# TODO: may want to split out seconds portion of delay and jitter
|
||||
event = CommEffectEvent()
|
||||
nem1 = self.session.emane.get_nem_id(iface)
|
||||
nem2 = self.session.emane.get_nem_id(iface2)
|
||||
logger.info("sending comm effect event")
|
||||
logging.info("sending comm effect event")
|
||||
event.append(
|
||||
nem1,
|
||||
latency=convert_none(options.delay),
|
||||
|
|
@ -139,4 +135,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
unicast=int(convert_none(options.bandwidth)),
|
||||
broadcast=int(convert_none(options.bandwidth)),
|
||||
)
|
||||
self.session.emane.publish_event(nem2, event)
|
||||
service.publish(nem2, event)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,9 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
manifest = None
|
||||
try:
|
||||
from emane.shell import manifest
|
||||
|
|
@ -14,7 +12,7 @@ except ImportError:
|
|||
from emanesh import manifest
|
||||
except ImportError:
|
||||
manifest = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def _type_value(config_type: str) -> ConfigDataTypes:
|
||||
|
|
@ -32,7 +30,7 @@ def _type_value(config_type: str) -> ConfigDataTypes:
|
|||
return ConfigDataTypes[config_type]
|
||||
|
||||
|
||||
def _get_possible(config_type: str, config_regex: str) -> list[str]:
|
||||
def _get_possible(config_type: str, config_regex: str) -> List[str]:
|
||||
"""
|
||||
Retrieve possible config value options based on emane regexes.
|
||||
|
||||
|
|
@ -50,7 +48,7 @@ def _get_possible(config_type: str, config_regex: str) -> list[str]:
|
|||
return []
|
||||
|
||||
|
||||
def _get_default(config_type_name: str, config_value: list[str]) -> str:
|
||||
def _get_default(config_type_name: str, config_value: List[str]) -> str:
|
||||
"""
|
||||
Convert default configuration values to one used by core.
|
||||
|
||||
|
|
@ -73,10 +71,9 @@ def _get_default(config_type_name: str, config_value: list[str]) -> str:
|
|||
return config_default
|
||||
|
||||
|
||||
def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]:
|
||||
def parse(manifest_path: str, 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 defaults: used to override default values for configurations
|
||||
|
|
@ -88,7 +85,7 @@ def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]:
|
|||
return []
|
||||
|
||||
# load configuration file
|
||||
manifest_file = manifest.Manifest(str(manifest_path))
|
||||
manifest_file = manifest.Manifest(manifest_path)
|
||||
manifest_configurations = manifest_file.getAllConfiguration()
|
||||
|
||||
configurations = []
|
||||
|
|
@ -119,8 +116,8 @@ def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]:
|
|||
config_descriptions = f"{config_descriptions} file"
|
||||
|
||||
configuration = Configuration(
|
||||
id=config_name,
|
||||
type=config_type_value,
|
||||
_id=config_name,
|
||||
_type=config_type_value,
|
||||
default=config_default,
|
||||
options=possible,
|
||||
label=config_descriptions,
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@
|
|||
Defines Emane Models used within CORE.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import os
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.emane import emanemanifest
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkOptions
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import WirelessModel
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
DEFAULT_DEV: str = "ctrl0"
|
||||
MANIFEST_PATH: str = "share/emane/manifest"
|
||||
|
||||
|
||||
class EmaneModel(WirelessModel):
|
||||
"""
|
||||
|
|
@ -25,104 +24,79 @@ class EmaneModel(WirelessModel):
|
|||
configurable parameters. Helper functions also live here.
|
||||
"""
|
||||
|
||||
# default platform configuration settings
|
||||
platform_controlport: str = "controlportendpoint"
|
||||
platform_xml: str = "nemmanager.xml"
|
||||
platform_defaults: dict[str, str] = {
|
||||
"eventservicedevice": DEFAULT_DEV,
|
||||
"eventservicegroup": "224.1.2.8:45703",
|
||||
"otamanagerdevice": DEFAULT_DEV,
|
||||
"otamanagergroup": "224.1.2.8:45702",
|
||||
}
|
||||
platform_config: list[Configuration] = []
|
||||
|
||||
# default mac configuration settings
|
||||
mac_library: Optional[str] = None
|
||||
mac_xml: Optional[str] = None
|
||||
mac_defaults: dict[str, str] = {}
|
||||
mac_config: list[Configuration] = []
|
||||
mac_defaults: Dict[str, str] = {}
|
||||
mac_config: List[Configuration] = []
|
||||
|
||||
# default phy configuration settings, using the universal model
|
||||
phy_library: Optional[str] = None
|
||||
phy_xml: str = "emanephy.xml"
|
||||
phy_defaults: dict[str, str] = {
|
||||
phy_defaults: Dict[str, str] = {
|
||||
"subid": "1",
|
||||
"propagationmodel": "2ray",
|
||||
"noisemode": "none",
|
||||
}
|
||||
phy_config: list[Configuration] = []
|
||||
phy_config: List[Configuration] = []
|
||||
|
||||
# support for external configurations
|
||||
external_config: list[Configuration] = [
|
||||
ConfigBool(id="external", default="0"),
|
||||
ConfigString(id="platformendpoint", default="127.0.0.1:40001"),
|
||||
ConfigString(id="transportendpoint", default="127.0.0.1:50002"),
|
||||
external_config: List[Configuration] = [
|
||||
Configuration("external", ConfigDataTypes.BOOL, default="0"),
|
||||
Configuration(
|
||||
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
|
||||
),
|
||||
Configuration(
|
||||
"transportendpoint", ConfigDataTypes.STRING, default="127.0.0.1:50002"
|
||||
),
|
||||
]
|
||||
|
||||
config_ignore: set[str] = set()
|
||||
config_ignore: Set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
"""
|
||||
Called after being loaded within the EmaneManager. Provides configured
|
||||
emane_prefix for parsing xml files.
|
||||
Called after being loaded within the EmaneManager. Provides configured emane_prefix for
|
||||
parsing xml files.
|
||||
|
||||
:param emane_prefix: configured emane prefix path
|
||||
:return: nothing
|
||||
"""
|
||||
cls._load_platform_config(emane_prefix)
|
||||
manifest_path = "share/emane/manifest"
|
||||
# load mac configuration
|
||||
mac_xml_path = emane_prefix / MANIFEST_PATH / cls.mac_xml
|
||||
mac_xml_path = os.path.join(emane_prefix, manifest_path, cls.mac_xml)
|
||||
cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults)
|
||||
|
||||
# load phy configuration
|
||||
phy_xml_path = emane_prefix / MANIFEST_PATH / cls.phy_xml
|
||||
phy_xml_path = os.path.join(emane_prefix, manifest_path, cls.phy_xml)
|
||||
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
|
||||
|
||||
@classmethod
|
||||
def _load_platform_config(cls, emane_prefix: Path) -> None:
|
||||
platform_xml_path = emane_prefix / MANIFEST_PATH / cls.platform_xml
|
||||
cls.platform_config = emanemanifest.parse(
|
||||
platform_xml_path, cls.platform_defaults
|
||||
)
|
||||
# remove controlport configuration, since core will set this directly
|
||||
controlport_index = None
|
||||
for index, configuration in enumerate(cls.platform_config):
|
||||
if configuration.id == cls.platform_controlport:
|
||||
controlport_index = index
|
||||
break
|
||||
if controlport_index is not None:
|
||||
cls.platform_config.pop(controlport_index)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
"""
|
||||
Returns the combination all all configurations (mac, phy, and external).
|
||||
|
||||
:return: all configurations
|
||||
"""
|
||||
return (
|
||||
cls.platform_config + cls.mac_config + cls.phy_config + cls.external_config
|
||||
)
|
||||
return cls.mac_config + cls.phy_config + cls.external_config
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
"""
|
||||
Returns the defined configuration groups.
|
||||
|
||||
:return: list of configuration groups.
|
||||
"""
|
||||
platform_len = len(cls.platform_config)
|
||||
mac_len = len(cls.mac_config) + platform_len
|
||||
mac_len = len(cls.mac_config)
|
||||
phy_len = len(cls.phy_config) + mac_len
|
||||
config_len = len(cls.configurations())
|
||||
return [
|
||||
ConfigGroup("Platform Parameters", 1, platform_len),
|
||||
ConfigGroup("MAC Parameters", platform_len + 1, mac_len),
|
||||
ConfigGroup("MAC Parameters", 1, mac_len),
|
||||
ConfigGroup("PHY Parameters", mac_len + 1, phy_len),
|
||||
ConfigGroup("External Parameters", phy_len + 1, config_len),
|
||||
]
|
||||
|
||||
def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
|
||||
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||
"""
|
||||
Builds xml files for this emane model. Creates a nem.xml file that points to
|
||||
both mac.xml and phy.xml definitions.
|
||||
|
|
@ -137,28 +111,29 @@ class EmaneModel(WirelessModel):
|
|||
emanexml.create_phy_xml(self, iface, config)
|
||||
emanexml.create_transport_xml(iface, config)
|
||||
|
||||
def post_startup(self, iface: CoreInterface) -> None:
|
||||
def post_startup(self) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
:param iface: interface for post startup
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug("emane model(%s) has no post setup tasks", self.name)
|
||||
logging.debug("emane model(%s) has no post setup tasks", self.name)
|
||||
|
||||
def update(self, moved_ifaces: list[CoreInterface]) -> None:
|
||||
def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Invoked from MobilityModel when nodes are moved; this causes
|
||||
emane location events to be generated for the nodes in the moved
|
||||
list, making EmaneModels compatible with Ns2ScriptedMobility.
|
||||
|
||||
:param moved: moved nodes
|
||||
:param moved_ifaces: interfaces that were moved
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
self.session.emane.set_nem_positions(moved_ifaces)
|
||||
wlan = self.session.get_node(self.id, EmaneNet)
|
||||
wlan.setnempositions(moved_ifaces)
|
||||
except CoreError:
|
||||
logger.exception("error during update")
|
||||
logging.exception("error during update")
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
|
@ -171,4 +146,4 @@ class EmaneModel(WirelessModel):
|
|||
:param iface2: interface two
|
||||
:return: nothing
|
||||
"""
|
||||
logger.warning("emane model(%s) does not support link config", self.name)
|
||||
logging.warning("emane model(%s) does not support link config", self.name)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
|
@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "ieee80211abgmaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
|
|
@ -2,17 +2,14 @@ import logging
|
|||
import sched
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkData
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags
|
||||
from core.nodes.network import CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane import shell
|
||||
except ImportError:
|
||||
|
|
@ -20,11 +17,12 @@ except ImportError:
|
|||
from emanesh import shell
|
||||
except ImportError:
|
||||
shell = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemanager import EmaneManager
|
||||
|
||||
DEFAULT_PORT: int = 47_000
|
||||
MAC_COMPONENT_INDEX: int = 1
|
||||
EMANE_RFPIPE: str = "rfpipemaclayer"
|
||||
EMANE_80211: str = "ieee80211abgmaclayer"
|
||||
|
|
@ -34,10 +32,10 @@ NEM_SELF: int = 65535
|
|||
|
||||
|
||||
class LossTable:
|
||||
def __init__(self, losses: dict[float, float]) -> None:
|
||||
self.losses: dict[float, float] = losses
|
||||
self.sinrs: list[float] = sorted(self.losses.keys())
|
||||
self.loss_lookup: dict[int, float] = {}
|
||||
def __init__(self, losses: Dict[float, float]) -> None:
|
||||
self.losses: Dict[float, float] = losses
|
||||
self.sinrs: List[float] = sorted(self.losses.keys())
|
||||
self.loss_lookup: Dict[int, float] = {}
|
||||
for index, value in enumerate(self.sinrs):
|
||||
self.loss_lookup[index] = self.losses[value]
|
||||
self.mac_id: Optional[str] = None
|
||||
|
|
@ -79,12 +77,12 @@ class EmaneLink:
|
|||
|
||||
|
||||
class EmaneClient:
|
||||
def __init__(self, address: str, port: int) -> None:
|
||||
def __init__(self, address: str) -> None:
|
||||
self.address: str = address
|
||||
self.client: shell.ControlPortClient = shell.ControlPortClient(
|
||||
self.address, port
|
||||
self.address, DEFAULT_PORT
|
||||
)
|
||||
self.nems: dict[int, LossTable] = {}
|
||||
self.nems: Dict[int, LossTable] = {}
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
|
|
@ -93,7 +91,7 @@ class EmaneClient:
|
|||
# get mac config
|
||||
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
|
||||
mac_config = self.client.getConfiguration(mac_id)
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
|
||||
)
|
||||
|
||||
|
|
@ -103,14 +101,14 @@ class EmaneClient:
|
|||
elif emane_model == EMANE_RFPIPE:
|
||||
loss_table = self.handle_rfpipe(mac_config)
|
||||
else:
|
||||
logger.warning("unknown emane link model: %s", emane_model)
|
||||
logging.warning("unknown emane link model: %s", emane_model)
|
||||
continue
|
||||
logger.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
|
||||
logging.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
|
||||
loss_table.mac_id = mac_id
|
||||
self.nems[nem_id] = loss_table
|
||||
|
||||
def check_links(
|
||||
self, links: dict[tuple[int, int], EmaneLink], loss_threshold: int
|
||||
self, links: Dict[Tuple[int, int], EmaneLink], loss_threshold: int
|
||||
) -> None:
|
||||
for from_nem, loss_table in self.nems.items():
|
||||
tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,))
|
||||
|
|
@ -138,14 +136,14 @@ class EmaneClient:
|
|||
link = EmaneLink(from_nem, to_nem, sinr)
|
||||
links[link_key] = link
|
||||
|
||||
def handle_tdma(self, config: dict[str, tuple]):
|
||||
def handle_tdma(self, config: Dict[str, Tuple]):
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("tdma pcr: %s", pcr)
|
||||
logging.debug("tdma pcr: %s", pcr)
|
||||
|
||||
def handle_80211(self, config: dict[str, tuple]) -> LossTable:
|
||||
def handle_80211(self, config: Dict[str, Tuple]) -> LossTable:
|
||||
unicastrate = config["unicastrate"][0][0]
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("80211 pcr: %s", pcr)
|
||||
logging.debug("80211 pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
|
@ -159,9 +157,9 @@ class EmaneClient:
|
|||
losses[sinr] = por
|
||||
return LossTable(losses)
|
||||
|
||||
def handle_rfpipe(self, config: dict[str, tuple]) -> LossTable:
|
||||
def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable:
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("rfpipe pcr: %s", pcr)
|
||||
logging.debug("rfpipe pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
|
@ -179,9 +177,9 @@ class EmaneClient:
|
|||
class EmaneLinkMonitor:
|
||||
def __init__(self, emane_manager: "EmaneManager") -> None:
|
||||
self.emane_manager: "EmaneManager" = emane_manager
|
||||
self.clients: list[EmaneClient] = []
|
||||
self.links: dict[tuple[int, int], EmaneLink] = {}
|
||||
self.complete_links: set[tuple[int, int]] = set()
|
||||
self.clients: List[EmaneClient] = []
|
||||
self.links: Dict[Tuple[int, int], EmaneLink] = {}
|
||||
self.complete_links: Set[Tuple[int, int]] = set()
|
||||
self.loss_threshold: Optional[int] = None
|
||||
self.link_interval: Optional[int] = None
|
||||
self.link_timeout: Optional[int] = None
|
||||
|
|
@ -189,13 +187,12 @@ class EmaneLinkMonitor:
|
|||
self.running: bool = False
|
||||
|
||||
def start(self) -> None:
|
||||
options = self.emane_manager.session.options
|
||||
self.loss_threshold = options.get_int("loss_threshold")
|
||||
self.link_interval = options.get_int("link_interval")
|
||||
self.link_timeout = options.get_int("link_timeout")
|
||||
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
|
||||
self.link_interval = int(self.emane_manager.get_config("link_interval"))
|
||||
self.link_timeout = int(self.emane_manager.get_config("link_timeout"))
|
||||
self.initialize()
|
||||
if not self.clients:
|
||||
logger.info("no valid emane models to monitor links")
|
||||
logging.info("no valid emane models to monitor links")
|
||||
return
|
||||
self.scheduler = sched.scheduler()
|
||||
self.scheduler.enter(0, 0, self.check_links)
|
||||
|
|
@ -205,28 +202,22 @@ class EmaneLinkMonitor:
|
|||
|
||||
def initialize(self) -> None:
|
||||
addresses = self.get_addresses()
|
||||
for address, port in addresses:
|
||||
client = EmaneClient(address, port)
|
||||
for address in addresses:
|
||||
client = EmaneClient(address)
|
||||
if client.nems:
|
||||
self.clients.append(client)
|
||||
|
||||
def get_addresses(self) -> list[tuple[str, int]]:
|
||||
def get_addresses(self) -> List[str]:
|
||||
addresses = []
|
||||
nodes = self.emane_manager.getnodes()
|
||||
for node in nodes:
|
||||
control = None
|
||||
ports = []
|
||||
for iface in node.get_ifaces():
|
||||
if isinstance(iface.net, CtrlNet):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
control = str(ip4.ip)
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
port = self.emane_manager.get_nem_port(iface)
|
||||
ports.append(port)
|
||||
if control:
|
||||
for port in ports:
|
||||
addresses.append((control, port))
|
||||
address = str(ip4.ip)
|
||||
addresses.append(address)
|
||||
break
|
||||
return addresses
|
||||
|
||||
def check_links(self) -> None:
|
||||
|
|
@ -237,7 +228,7 @@ class EmaneLinkMonitor:
|
|||
client.check_links(self.links, self.loss_threshold)
|
||||
except shell.ControlPortException:
|
||||
if self.running:
|
||||
logger.exception("link monitor error")
|
||||
logging.exception("link monitor error")
|
||||
|
||||
# find new links
|
||||
current_links = set(self.links.keys())
|
||||
|
|
@ -273,25 +264,25 @@ class EmaneLinkMonitor:
|
|||
if self.running:
|
||||
self.scheduler.enter(self.link_interval, 0, self.check_links)
|
||||
|
||||
def get_complete_id(self, link_id: tuple[int, int]) -> tuple[int, int]:
|
||||
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
|
||||
value1, value2 = link_id
|
||||
if value1 < value2:
|
||||
return value1, value2
|
||||
else:
|
||||
return value2, value1
|
||||
|
||||
def is_complete_link(self, link_id: tuple[int, int]) -> bool:
|
||||
def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
|
||||
reverse_id = link_id[1], link_id[0]
|
||||
return link_id in self.links and reverse_id in self.links
|
||||
|
||||
def get_link_label(self, link_id: tuple[int, int]) -> str:
|
||||
def get_link_label(self, link_id: Tuple[int, int]) -> str:
|
||||
source_id = tuple(sorted(link_id))
|
||||
source_link = self.links[source_id]
|
||||
dest_id = link_id[::-1]
|
||||
dest_link = self.links[dest_id]
|
||||
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
|
||||
|
||||
def send_link(self, message_type: MessageFlags, link_id: tuple[int, int]) -> None:
|
||||
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
|
||||
nem1, nem2 = link_id
|
||||
link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
|
||||
if link:
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
import logging
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
from core import utils
|
||||
from core.emane import models as emane_models
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmaneModelManager:
|
||||
models: dict[str, type[EmaneModel]] = {}
|
||||
|
||||
@classmethod
|
||||
def load_locals(cls, emane_prefix: Path) -> list[str]:
|
||||
"""
|
||||
Load local core emane models and make them available.
|
||||
|
||||
:param emane_prefix: installed emane prefix
|
||||
:return: list of errors encountered loading emane models
|
||||
"""
|
||||
errors = []
|
||||
for module_info in pkgutil.walk_packages(
|
||||
emane_models.__path__, f"{emane_models.__name__}."
|
||||
):
|
||||
models = utils.load_module(module_info.name, EmaneModel)
|
||||
for model in models:
|
||||
logger.debug("loading emane model: %s", model.name)
|
||||
try:
|
||||
model.load(emane_prefix)
|
||||
cls.models[model.name] = model
|
||||
except CoreError as e:
|
||||
errors.append(model.name)
|
||||
logger.debug("not loading emane model(%s): %s", model.name, e)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path, emane_prefix: Path) -> list[str]:
|
||||
"""
|
||||
Search and load custom emane models and make them available.
|
||||
|
||||
:param path: path to search for custom emane models
|
||||
:param emane_prefix: installed emane prefix
|
||||
:return: list of errors encountered loading emane models
|
||||
"""
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
errors = []
|
||||
for subdir in subdirs:
|
||||
logger.debug("loading emane models from: %s", subdir)
|
||||
models = utils.load_classes(subdir, EmaneModel)
|
||||
for model in models:
|
||||
logger.debug("loading emane model: %s", model.name)
|
||||
try:
|
||||
model.load(emane_prefix)
|
||||
cls.models[model.name] = model
|
||||
except CoreError as e:
|
||||
errors.append(model.name)
|
||||
logger.debug("not loading emane model(%s): %s", model.name, e)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> type[EmaneModel]:
|
||||
model = cls.models.get(name)
|
||||
if model is None:
|
||||
raise CoreError(f"emame model does not exist {name}")
|
||||
return model
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""
|
||||
tdma.py: EMANE TDMA model bindings for CORE
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from core import constants, utils
|
||||
from core.config import ConfigString
|
||||
from core.emane import emanemodel
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmaneTdmaModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_tdma"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "tdmaeventschedulerradiomodel"
|
||||
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
|
||||
|
||||
# add custom schedule options and ignore it when writing emane xml
|
||||
schedule_name: str = "schedule"
|
||||
default_schedule: Path = (
|
||||
constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml"
|
||||
)
|
||||
config_ignore: set[str] = {schedule_name}
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix
|
||||
/ "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
config_item = ConfigString(
|
||||
id=cls.schedule_name,
|
||||
default=str(cls.default_schedule),
|
||||
label="TDMA schedule file (core)",
|
||||
)
|
||||
cls.mac_config.insert(0, config_item)
|
||||
|
||||
def post_startup(self, iface: CoreInterface) -> None:
|
||||
# get configured schedule
|
||||
emane_net = self.session.get_node(self.id, EmaneNet)
|
||||
config = self.session.emane.get_iface_config(emane_net, iface)
|
||||
schedule = Path(config[self.schedule_name])
|
||||
if not schedule.is_file():
|
||||
logger.error("ignoring invalid tdma schedule: %s", schedule)
|
||||
return
|
||||
# initiate tdma schedule
|
||||
nem_id = self.session.emane.get_nem_id(iface)
|
||||
if not nem_id:
|
||||
logger.error("could not find nem for interface")
|
||||
return
|
||||
service = self.session.emane.nem_service.get(nem_id)
|
||||
if service:
|
||||
device = service.device
|
||||
logger.info(
|
||||
"setting up tdma schedule: schedule(%s) device(%s)", schedule, device
|
||||
)
|
||||
utils.cmd(f"emaneevent-tdmaschedule -i {device} {schedule}")
|
||||
|
|
@ -4,23 +4,28 @@ share the same MAC+PHY model.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.enumerations import MessageFlags, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions
|
||||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NodeTypes,
|
||||
RegisterTlvs,
|
||||
)
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WayPointMobility
|
||||
from core.location.mobility import WirelessModel, WayPointMobility
|
||||
|
||||
OptionalEmaneModel = Optional[EmaneModel]
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
try:
|
||||
from emane.events import LocationEvent
|
||||
|
|
@ -29,121 +34,7 @@ except ImportError:
|
|||
from emanesh.events import LocationEvent
|
||||
except ImportError:
|
||||
LocationEvent = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
class TunTap(CoreInterface):
|
||||
"""
|
||||
TUN/TAP virtual device in TAP mode
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: int,
|
||||
name: str,
|
||||
localname: str,
|
||||
use_ovs: bool,
|
||||
node: CoreNode = None,
|
||||
server: "DistributedServer" = None,
|
||||
) -> None:
|
||||
super().__init__(_id, name, localname, use_ovs, node=node, server=server)
|
||||
self.node: CoreNode = node
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
Startup logic for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown functionality for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
self.up = False
|
||||
|
||||
def waitfor(
|
||||
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
|
||||
) -> bool:
|
||||
"""
|
||||
Wait for func() to return zero with exponential backoff.
|
||||
|
||||
:param func: function to wait for a result of zero
|
||||
:param attempts: number of attempts to wait for a zero result
|
||||
:param maxretrydelay: maximum retry delay
|
||||
:return: True if wait succeeded, False otherwise
|
||||
"""
|
||||
delay = 0.01
|
||||
result = False
|
||||
for i in range(1, attempts + 1):
|
||||
r = func()
|
||||
if r == 0:
|
||||
result = True
|
||||
break
|
||||
msg = f"attempt {i} failed with nonzero exit status {r}"
|
||||
if i < attempts + 1:
|
||||
msg += ", retrying..."
|
||||
logger.info(msg)
|
||||
time.sleep(delay)
|
||||
delay += delay
|
||||
if delay > maxretrydelay:
|
||||
delay = maxretrydelay
|
||||
else:
|
||||
msg += ", giving up"
|
||||
logger.info(msg)
|
||||
return result
|
||||
|
||||
def nodedevexists(self) -> int:
|
||||
"""
|
||||
Checks if device exists.
|
||||
|
||||
:return: 0 if device exists, 1 otherwise
|
||||
"""
|
||||
try:
|
||||
self.node.node_net_client.device_show(self.name)
|
||||
return 0
|
||||
except CoreCommandError:
|
||||
return 1
|
||||
|
||||
def waitfordevicenode(self) -> None:
|
||||
"""
|
||||
Check for presence of a node device - tap device may not appear right away waits.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug("waiting for device node: %s", self.name)
|
||||
count = 0
|
||||
while True:
|
||||
result = self.waitfor(self.nodedevexists)
|
||||
if result:
|
||||
break
|
||||
should_retry = count < 5
|
||||
is_emane_running = self.node.session.emane.emanerunning(self.node)
|
||||
if all([should_retry, is_emane_running]):
|
||||
count += 1
|
||||
else:
|
||||
raise RuntimeError("node device failed to exist")
|
||||
|
||||
def set_ips(self) -> None:
|
||||
"""
|
||||
Set interface ip addresses.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.waitfordevicenode()
|
||||
for ip in self.ips():
|
||||
self.node.node_net_client.create_address(self.name, str(ip))
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmaneOptions(NodeOptions):
|
||||
emane_model: str = None
|
||||
"""name of emane model to associate an emane network to"""
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
class EmaneNet(CoreNetworkBase):
|
||||
|
|
@ -153,26 +44,22 @@ class EmaneNet(CoreNetworkBase):
|
|||
Emane controller object that exists in a session.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.EMANE
|
||||
linktype: LinkTypes = LinkTypes.WIRED
|
||||
type: str = "wlan"
|
||||
has_custom_iface: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
server: DistributedServer = None,
|
||||
options: EmaneOptions = None,
|
||||
) -> None:
|
||||
options = options or EmaneOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
super().__init__(session, _id, name, server)
|
||||
self.conf: str = ""
|
||||
self.model: "OptionalEmaneModel" = None
|
||||
self.mobility: Optional[WayPointMobility] = None
|
||||
model_class = self.session.emane.get_model(options.emane_model)
|
||||
self.wireless_model: Optional["EmaneModel"] = model_class(self.session, self.id)
|
||||
if self.session.is_running():
|
||||
self.session.emane.add_node(self)
|
||||
|
||||
@classmethod
|
||||
def create_options(cls) -> EmaneOptions:
|
||||
return EmaneOptions()
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
|
@ -180,15 +67,18 @@ class EmaneNet(CoreNetworkBase):
|
|||
"""
|
||||
The CommEffect model supports link configuration.
|
||||
"""
|
||||
if not self.wireless_model:
|
||||
if not self.model:
|
||||
return
|
||||
self.wireless_model.linkconfig(iface, options, iface2)
|
||||
self.model.linkconfig(iface, options, iface2)
|
||||
|
||||
def config(self, conf: str) -> None:
|
||||
self.conf = conf
|
||||
|
||||
def startup(self) -> None:
|
||||
self.up = True
|
||||
pass
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.up = False
|
||||
pass
|
||||
|
||||
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
|
@ -196,37 +86,93 @@ class EmaneNet(CoreNetworkBase):
|
|||
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def updatemodel(self, config: dict[str, str]) -> None:
|
||||
"""
|
||||
Update configuration for the current model.
|
||||
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
||||
raise CoreError("emane networks cannot be linked to other networks")
|
||||
|
||||
:param config: configuration to update model with
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.wireless_model:
|
||||
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||
if not self.model:
|
||||
raise CoreError(f"no model set to update for node({self.name})")
|
||||
logger.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.wireless_model.name, config
|
||||
logging.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
||||
)
|
||||
self.wireless_model.update_config(config)
|
||||
self.model.update_config(config)
|
||||
|
||||
def setmodel(
|
||||
self,
|
||||
model: Union[type["EmaneModel"], type["WayPointMobility"]],
|
||||
config: dict[str, str],
|
||||
) -> None:
|
||||
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
|
||||
"""
|
||||
set the EmaneModel associated with this node
|
||||
"""
|
||||
if model.config_type == RegisterTlvs.WIRELESS:
|
||||
self.wireless_model = model(session=self.session, _id=self.id)
|
||||
self.wireless_model.update_config(config)
|
||||
# EmaneModel really uses values from ConfigurableManager
|
||||
# when buildnemxml() is called, not during init()
|
||||
self.model = model(session=self.session, _id=self.id)
|
||||
self.model.update_config(config)
|
||||
elif model.config_type == RegisterTlvs.MOBILITY:
|
||||
self.mobility = model(session=self.session, _id=self.id)
|
||||
self.mobility.update_config(config)
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
|
||||
links = []
|
||||
def _nem_position(
|
||||
self, iface: CoreInterface
|
||||
) -> Optional[Tuple[int, float, float, float]]:
|
||||
"""
|
||||
Creates nem position for emane event for a given interface.
|
||||
|
||||
:param iface: interface to get nem emane position for
|
||||
:return: nem position tuple, None otherwise
|
||||
"""
|
||||
nem_id = self.session.emane.get_nem_id(iface)
|
||||
ifname = iface.localname
|
||||
if nem_id is None:
|
||||
logging.info("nemid for %s is unknown", ifname)
|
||||
return
|
||||
node = iface.node
|
||||
x, y, z = node.getposition()
|
||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||
if node.position.alt is not None:
|
||||
alt = node.position.alt
|
||||
node.position.set_geo(lon, lat, alt)
|
||||
# altitude must be an integer or warning is printed
|
||||
alt = int(round(alt))
|
||||
return nem_id, lon, lat, alt
|
||||
|
||||
def setnemposition(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
Publish a NEM location change event using the EMANE event service.
|
||||
|
||||
:param iface: interface to set nem position for
|
||||
"""
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
position = self._nem_position(iface)
|
||||
if position:
|
||||
nemid, lon, lat, alt = position
|
||||
event = LocationEvent()
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
||||
calculation. Generate an EMANE Location Event having several
|
||||
entries for each interface that has moved.
|
||||
"""
|
||||
if len(moved_ifaces) == 0:
|
||||
return
|
||||
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
|
||||
event = LocationEvent()
|
||||
for iface in moved_ifaces:
|
||||
position = self._nem_position(iface)
|
||||
if position:
|
||||
nemid, lon, lat, alt = position
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
links = super().links(flags)
|
||||
emane_manager = self.session.emane
|
||||
# gather current emane links
|
||||
nem_ids = set()
|
||||
|
|
@ -247,44 +193,22 @@ class EmaneNet(CoreNetworkBase):
|
|||
# ignore incomplete links
|
||||
if (nem2, nem1) not in emane_links:
|
||||
continue
|
||||
link = emane_manager.get_nem_link(nem1, nem2, flags)
|
||||
link = emane_manager.get_nem_link(nem1, nem2)
|
||||
if link:
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
"""
|
||||
Create a tuntap interface for the provided node.
|
||||
|
||||
:param node: node to create tuntap interface for
|
||||
:param iface_data: interface data to create interface with
|
||||
:return: created tuntap interface
|
||||
"""
|
||||
with node.lock:
|
||||
if iface_data.id is not None and iface_data.id in node.ifaces:
|
||||
raise CoreError(
|
||||
f"node({self.id}) interface({iface_data.id}) already exists"
|
||||
)
|
||||
iface_id = (
|
||||
iface_data.id if iface_data.id is not None else node.next_iface_id()
|
||||
)
|
||||
name = iface_data.name if iface_data.name is not None else f"eth{iface_id}"
|
||||
session_id = self.session.short_session_id()
|
||||
localname = f"tap{node.id}.{iface_id}.{session_id}"
|
||||
iface = TunTap(iface_id, name, localname, self.session.use_ovs(), node=node)
|
||||
if iface_data.mac:
|
||||
iface.set_mac(iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
iface.add_ip(ip)
|
||||
node.ifaces[iface_id] = iface
|
||||
self.attach(iface)
|
||||
if self.up:
|
||||
iface.startup()
|
||||
if self.session.is_running():
|
||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
# TUN/TAP is not ready for addressing yet; the device may
|
||||
# take some time to appear, and installing it into a
|
||||
# namespace after it has been bound removes addressing;
|
||||
# save addresses with the interface now
|
||||
iface_id = node.newtuntap(iface_data.id, iface_data.name)
|
||||
node.attachnet(iface_id, self)
|
||||
iface = node.get_iface(iface_id)
|
||||
iface.set_mac(iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
iface.add_ip(ip)
|
||||
if self.session.state == EventTypes.RUNTIME_STATE:
|
||||
self.session.emane.start_iface(self, iface)
|
||||
return iface
|
||||
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
raise CoreError(
|
||||
f"emane network({self.name}) do not support adopting interfaces"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
rfpipe.py: EMANE RF-PIPE model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
|
@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "rfpipemaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
67
daemon/core/emane/tdma.py
Normal file
67
daemon/core/emane/tdma.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
tdma.py: EMANE TDMA model bindings for CORE
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Set
|
||||
|
||||
from core import constants, utils
|
||||
from core.config import Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class EmaneTdmaModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_tdma"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "tdmaeventschedulerradiomodel"
|
||||
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
|
||||
|
||||
# add custom schedule options and ignore it when writing emane xml
|
||||
schedule_name: str = "schedule"
|
||||
default_schedule: str = os.path.join(
|
||||
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
|
||||
)
|
||||
config_ignore: Set[str] = {schedule_name}
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix,
|
||||
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml",
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
cls.mac_config.insert(
|
||||
0,
|
||||
Configuration(
|
||||
_id=cls.schedule_name,
|
||||
_type=ConfigDataTypes.STRING,
|
||||
default=cls.default_schedule,
|
||||
label="TDMA schedule file (core)",
|
||||
),
|
||||
)
|
||||
|
||||
def post_startup(self) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# get configured schedule
|
||||
config = self.session.emane.get_configs(node_id=self.id, config_type=self.name)
|
||||
if not config:
|
||||
return
|
||||
schedule = config[self.schedule_name]
|
||||
|
||||
# get the set event device
|
||||
event_device = self.session.emane.event_device
|
||||
|
||||
# initiate tdma schedule
|
||||
logging.info(
|
||||
"setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device
|
||||
)
|
||||
args = f"emaneevent-tdmaschedule -i {event_device} {schedule}"
|
||||
utils.cmd(args)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
from collections.abc import Callable
|
||||
from typing import TypeVar, Union
|
||||
|
||||
from core.emulator.data import (
|
||||
ConfigData,
|
||||
EventData,
|
||||
ExceptionData,
|
||||
FileData,
|
||||
LinkData,
|
||||
NodeData,
|
||||
)
|
||||
from core.errors import CoreError
|
||||
|
||||
T = TypeVar(
|
||||
"T", bound=Union[EventData, ExceptionData, NodeData, LinkData, FileData, ConfigData]
|
||||
)
|
||||
|
||||
|
||||
class BroadcastManager:
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Creates a BroadcastManager instance.
|
||||
"""
|
||||
self.handlers: dict[type[T], set[Callable[[T], None]]] = {}
|
||||
|
||||
def send(self, data: T) -> None:
|
||||
"""
|
||||
Retrieve handlers for data, and run all current handlers.
|
||||
|
||||
:param data: data to provide to handlers
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.get(type(data), set())
|
||||
for handler in handlers:
|
||||
handler(data)
|
||||
|
||||
def add_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
|
||||
"""
|
||||
Add a handler for a given data type.
|
||||
|
||||
:param data_type: type of data to add handler for
|
||||
:param handler: handler to add
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.setdefault(data_type, set())
|
||||
if handler in handlers:
|
||||
raise CoreError(
|
||||
f"cannot add data({data_type}) handler({repr(handler)}), "
|
||||
f"already exists"
|
||||
)
|
||||
handlers.add(handler)
|
||||
|
||||
def remove_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
|
||||
"""
|
||||
Remove a handler for a given data type.
|
||||
|
||||
:param data_type: type of data to remove handler for
|
||||
:param handler: handler to remove
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.get(data_type, set())
|
||||
if handler not in handlers:
|
||||
raise CoreError(
|
||||
f"cannot remove data({data_type}) handler({repr(handler)}), "
|
||||
f"does not exist"
|
||||
)
|
||||
handlers.remove(handler)
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core import utils
|
||||
from core.emulator.data import InterfaceData
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.interface import DEFAULT_MTU
|
||||
from core.nodes.network import CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
CTRL_NET_ID: int = 9001
|
||||
ETC_HOSTS_PATH: str = "/etc/hosts"
|
||||
|
||||
|
||||
class ControlNetManager:
|
||||
def __init__(self, session: "Session") -> None:
|
||||
self.session: "Session" = session
|
||||
self.etc_hosts_header: str = f"CORE session {self.session.id} host entries"
|
||||
|
||||
def _etc_hosts_enabled(self) -> bool:
|
||||
"""
|
||||
Determines if /etc/hosts should be configured.
|
||||
|
||||
:return: True if /etc/hosts should be configured, False otherwise
|
||||
"""
|
||||
return self.session.options.get_bool("update_etc_hosts", False)
|
||||
|
||||
def _get_server_ifaces(
|
||||
self,
|
||||
) -> tuple[None, Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Retrieve control net server interfaces.
|
||||
|
||||
:return: control net server interfaces
|
||||
"""
|
||||
d0 = self.session.options.get("controlnetif0")
|
||||
if d0:
|
||||
logger.error("controlnet0 cannot be assigned with a host interface")
|
||||
d1 = self.session.options.get("controlnetif1")
|
||||
d2 = self.session.options.get("controlnetif2")
|
||||
d3 = self.session.options.get("controlnetif3")
|
||||
return None, d1, d2, d3
|
||||
|
||||
def _get_prefixes(
|
||||
self,
|
||||
) -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Retrieve control net prefixes.
|
||||
|
||||
:return: control net prefixes
|
||||
"""
|
||||
p = self.session.options.get("controlnet")
|
||||
p0 = self.session.options.get("controlnet0")
|
||||
p1 = self.session.options.get("controlnet1")
|
||||
p2 = self.session.options.get("controlnet2")
|
||||
p3 = self.session.options.get("controlnet3")
|
||||
if not p0 and p:
|
||||
p0 = p
|
||||
return p0, p1, p2, p3
|
||||
|
||||
def update_etc_hosts(self) -> None:
|
||||
"""
|
||||
Add the IP addresses of control interfaces to the /etc/hosts file.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self._etc_hosts_enabled():
|
||||
return
|
||||
control_net = self.get_control_net(0)
|
||||
entries = ""
|
||||
for iface in control_net.get_ifaces():
|
||||
name = iface.node.name
|
||||
for ip in iface.ips():
|
||||
entries += f"{ip.ip} {name}\n"
|
||||
logger.info("adding entries to /etc/hosts")
|
||||
utils.file_munge(ETC_HOSTS_PATH, self.etc_hosts_header, entries)
|
||||
|
||||
def clear_etc_hosts(self) -> None:
|
||||
"""
|
||||
Clear IP addresses of control interfaces from the /etc/hosts file.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self._etc_hosts_enabled():
|
||||
return
|
||||
logger.info("removing /etc/hosts file entries")
|
||||
utils.file_demunge(ETC_HOSTS_PATH, self.etc_hosts_header)
|
||||
|
||||
def get_control_net_index(self, dev: str) -> int:
|
||||
"""
|
||||
Retrieve control net index.
|
||||
|
||||
:param dev: device to get control net index for
|
||||
:return: control net index, -1 otherwise
|
||||
"""
|
||||
if dev[0:4] == "ctrl" and int(dev[4]) in (0, 1, 2, 3):
|
||||
index = int(dev[4])
|
||||
if index == 0:
|
||||
return index
|
||||
if index < 4 and self._get_prefixes()[index] is not None:
|
||||
return index
|
||||
return -1
|
||||
|
||||
def get_control_net(self, index: int) -> Optional[CtrlNet]:
|
||||
"""
|
||||
Retrieve a control net based on index.
|
||||
|
||||
:param index: control net index
|
||||
:return: control net when available, None otherwise
|
||||
"""
|
||||
try:
|
||||
return self.session.get_node(CTRL_NET_ID + index, CtrlNet)
|
||||
except CoreError:
|
||||
return None
|
||||
|
||||
def add_control_net(
|
||||
self, index: int, conf_required: bool = True
|
||||
) -> Optional[CtrlNet]:
|
||||
"""
|
||||
Create a control network bridge as necessary. The conf_reqd flag,
|
||||
when False, causes a control network bridge to be added even if
|
||||
one has not been configured.
|
||||
|
||||
:param index: network index to add
|
||||
:param conf_required: flag to check if conf is required
|
||||
:return: control net node
|
||||
"""
|
||||
logger.info(
|
||||
"checking to add control net index(%s) conf_required(%s)",
|
||||
index,
|
||||
conf_required,
|
||||
)
|
||||
# check for valid index
|
||||
if not (0 <= index <= 3):
|
||||
raise CoreError(f"invalid control net index({index})")
|
||||
# return any existing control net bridge
|
||||
control_net = self.get_control_net(index)
|
||||
if control_net:
|
||||
logger.info("control net index(%s) already exists", index)
|
||||
return control_net
|
||||
# retrieve prefix for current index
|
||||
index_prefix = self._get_prefixes()[index]
|
||||
if not index_prefix:
|
||||
if conf_required:
|
||||
return None
|
||||
else:
|
||||
index_prefix = CtrlNet.DEFAULT_PREFIX_LIST[index]
|
||||
# retrieve valid prefix from old style values
|
||||
prefixes = index_prefix.split()
|
||||
if len(prefixes) > 1:
|
||||
# a list of per-host prefixes is provided
|
||||
try:
|
||||
prefix = prefixes[0].split(":", 1)[1]
|
||||
except IndexError:
|
||||
prefix = prefixes[0]
|
||||
else:
|
||||
prefix = prefixes[0]
|
||||
# use the updown script for control net 0 only
|
||||
updown_script = None
|
||||
if index == 0:
|
||||
updown_script = self.session.options.get("controlnet_updown_script")
|
||||
# build a new controlnet bridge
|
||||
_id = CTRL_NET_ID + index
|
||||
server_iface = self._get_server_ifaces()[index]
|
||||
logger.info(
|
||||
"adding controlnet(%s) prefix(%s) updown(%s) server interface(%s)",
|
||||
_id,
|
||||
prefix,
|
||||
updown_script,
|
||||
server_iface,
|
||||
)
|
||||
options = CtrlNet.create_options()
|
||||
options.prefix = prefix
|
||||
options.updown_script = updown_script
|
||||
options.serverintf = server_iface
|
||||
control_net = self.session.create_node(CtrlNet, False, _id, options=options)
|
||||
control_net.brname = f"ctrl{index}.{self.session.short_session_id()}"
|
||||
control_net.startup()
|
||||
return control_net
|
||||
|
||||
def remove_control_net(self, index: int) -> None:
|
||||
"""
|
||||
Removes control net.
|
||||
|
||||
:param index: index of control net to remove
|
||||
:return: nothing
|
||||
"""
|
||||
control_net = self.get_control_net(index)
|
||||
if control_net:
|
||||
logger.info("removing control net index(%s)", index)
|
||||
self.session.delete_node(control_net.id)
|
||||
|
||||
def add_control_iface(self, node: CoreNode, index: int) -> None:
|
||||
"""
|
||||
Adds a control net interface to a node.
|
||||
|
||||
:param node: node to add control net interface to
|
||||
:param index: index of control net to add interface to
|
||||
:return: nothing
|
||||
:raises CoreError: if control net doesn't exist, interface already exists,
|
||||
or there is an error creating the interface
|
||||
"""
|
||||
control_net = self.get_control_net(index)
|
||||
if not control_net:
|
||||
raise CoreError(f"control net index({index}) does not exist")
|
||||
iface_id = control_net.CTRLIF_IDX_BASE + index
|
||||
if node.ifaces.get(iface_id):
|
||||
raise CoreError(f"control iface({iface_id}) already exists")
|
||||
try:
|
||||
logger.info(
|
||||
"node(%s) adding control net index(%s) interface(%s)",
|
||||
node.name,
|
||||
index,
|
||||
iface_id,
|
||||
)
|
||||
ip4 = control_net.prefix[node.id]
|
||||
ip4_mask = control_net.prefix.prefixlen
|
||||
iface_data = InterfaceData(
|
||||
id=iface_id,
|
||||
name=f"ctrl{index}",
|
||||
mac=utils.random_mac(),
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
mtu=DEFAULT_MTU,
|
||||
)
|
||||
iface = node.create_iface(iface_data)
|
||||
control_net.attach(iface)
|
||||
iface.control = True
|
||||
except ValueError:
|
||||
raise CoreError(
|
||||
f"error adding control net interface to node({node.id}), "
|
||||
f"invalid control net prefix({control_net.prefix}), "
|
||||
"a longer prefix length may be required"
|
||||
)
|
||||
|
|
@ -1,17 +1,35 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import sys
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from core import utils
|
||||
import core.services
|
||||
from core import configservices, utils
|
||||
from core.configservice.manager import ConfigServiceManager
|
||||
from core.emane.modelmanager import EmaneModelManager
|
||||
from core.emulator.session import Session
|
||||
from core.executables import get_requirements
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_EMANE_PREFIX: str = "/usr"
|
||||
def signal_handler(signal_number: int, _) -> None:
|
||||
"""
|
||||
Handle signals and force an exit with cleanup.
|
||||
|
||||
:param signal_number: signal number
|
||||
:param _: ignored
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("caught signal: %s", signal_number)
|
||||
sys.exit(signal_number)
|
||||
|
||||
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGUSR1, signal_handler)
|
||||
signal.signal(signal.SIGUSR2, signal_handler)
|
||||
|
||||
|
||||
class CoreEmu:
|
||||
|
|
@ -19,7 +37,7 @@ class CoreEmu:
|
|||
Provides logic for creating and configuring CORE sessions and the nodes within them.
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict[str, str] = None) -> None:
|
||||
def __init__(self, config: Dict[str, str] = None) -> None:
|
||||
"""
|
||||
Create a CoreEmu object.
|
||||
|
||||
|
|
@ -29,24 +47,31 @@ class CoreEmu:
|
|||
os.umask(0)
|
||||
|
||||
# configuration
|
||||
config = config if config else {}
|
||||
self.config: dict[str, str] = config
|
||||
if config is None:
|
||||
config = {}
|
||||
self.config: Dict[str, str] = config
|
||||
|
||||
# session management
|
||||
self.sessions: dict[int, Session] = {}
|
||||
self.sessions: Dict[int, Session] = {}
|
||||
|
||||
# load services
|
||||
self.service_errors: list[str] = []
|
||||
self.service_manager: ConfigServiceManager = ConfigServiceManager()
|
||||
self._load_services()
|
||||
self.service_errors: List[str] = []
|
||||
self.load_services()
|
||||
|
||||
# check and load emane
|
||||
self.has_emane: bool = False
|
||||
self._load_emane()
|
||||
# config services
|
||||
self.service_manager: ConfigServiceManager = ConfigServiceManager()
|
||||
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
|
||||
self.service_manager.load(config_services_path)
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir:
|
||||
self.service_manager.load(custom_dir)
|
||||
|
||||
# check executables exist on path
|
||||
self._validate_env()
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def _validate_env(self) -> None:
|
||||
"""
|
||||
Validates executables CORE depends on exist on path.
|
||||
|
|
@ -58,54 +83,23 @@ class CoreEmu:
|
|||
for requirement in get_requirements(use_ovs):
|
||||
utils.which(requirement, required=True)
|
||||
|
||||
def _load_services(self) -> None:
|
||||
def load_services(self) -> None:
|
||||
"""
|
||||
Loads default and custom services for use within CORE.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# load default services
|
||||
self.service_errors = ServiceManager.load_locals()
|
||||
self.service_errors = core.services.load()
|
||||
|
||||
# load custom services
|
||||
service_paths = self.config.get("custom_services_dir")
|
||||
logger.debug("custom service paths: %s", service_paths)
|
||||
if service_paths is not None:
|
||||
logging.debug("custom service paths: %s", service_paths)
|
||||
if service_paths:
|
||||
for service_path in service_paths.split(","):
|
||||
service_path = Path(service_path.strip())
|
||||
service_path = service_path.strip()
|
||||
custom_service_errors = ServiceManager.add_services(service_path)
|
||||
self.service_errors.extend(custom_service_errors)
|
||||
# load default config services
|
||||
self.service_manager.load_locals()
|
||||
# load custom config services
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir is not None:
|
||||
custom_dir = Path(custom_dir)
|
||||
self.service_manager.load(custom_dir)
|
||||
|
||||
def _load_emane(self) -> None:
|
||||
"""
|
||||
Check if emane is installed and load models.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# check for emane
|
||||
path = utils.which("emane", required=False)
|
||||
self.has_emane = path is not None
|
||||
if not self.has_emane:
|
||||
logger.info("emane is not installed, emane functionality disabled")
|
||||
return
|
||||
# get version
|
||||
emane_version = utils.cmd("emane --version")
|
||||
logger.info("using emane: %s", emane_version)
|
||||
emane_prefix = self.config.get("emane_prefix", DEFAULT_EMANE_PREFIX)
|
||||
emane_prefix = Path(emane_prefix)
|
||||
EmaneModelManager.load_locals(emane_prefix)
|
||||
# load custom models
|
||||
custom_path = self.config.get("emane_models_dir")
|
||||
if custom_path is not None:
|
||||
logger.info("loading custom emane models: %s", custom_path)
|
||||
custom_path = Path(custom_path)
|
||||
EmaneModelManager.load(custom_path, emane_prefix)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
|
|
@ -113,12 +107,14 @@ class CoreEmu:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logger.info("shutting down all sessions")
|
||||
while self.sessions:
|
||||
_, session = self.sessions.popitem()
|
||||
logging.info("shutting down all sessions")
|
||||
sessions = self.sessions.copy()
|
||||
self.sessions.clear()
|
||||
for _id in sessions:
|
||||
session = sessions[_id]
|
||||
session.shutdown()
|
||||
|
||||
def create_session(self, _id: int = None, _cls: type[Session] = Session) -> Session:
|
||||
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session:
|
||||
"""
|
||||
Create a new CORE session.
|
||||
|
||||
|
|
@ -132,7 +128,7 @@ class CoreEmu:
|
|||
_id += 1
|
||||
session = _cls(_id, config=self.config)
|
||||
session.service_manager = self.service_manager
|
||||
logger.info("created session: %s", _id)
|
||||
logging.info("created session: %s", _id)
|
||||
self.sessions[_id] = session
|
||||
return session
|
||||
|
||||
|
|
@ -143,14 +139,13 @@ class CoreEmu:
|
|||
:param _id: session id to delete
|
||||
:return: True if deleted, False otherwise
|
||||
"""
|
||||
logger.info("deleting session: %s", _id)
|
||||
logging.info("deleting session: %s", _id)
|
||||
session = self.sessions.pop(_id, None)
|
||||
result = False
|
||||
if session:
|
||||
logger.info("shutting session down: %s", _id)
|
||||
session.data_collect()
|
||||
logging.info("shutting session down: %s", _id)
|
||||
session.shutdown()
|
||||
result = True
|
||||
else:
|
||||
logger.error("session to delete did not exist: %s", _id)
|
||||
logging.error("session to delete did not exist: %s", _id)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
CORE data objects.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import netaddr
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ class ConfigData:
|
|||
node: int = None
|
||||
object: str = None
|
||||
type: int = None
|
||||
data_types: tuple[int] = None
|
||||
data_types: Tuple[int] = None
|
||||
data_values: str = None
|
||||
captions: str = None
|
||||
bitmap: str = None
|
||||
|
|
@ -81,8 +81,8 @@ class NodeOptions:
|
|||
model: Optional[str] = "PC"
|
||||
canvas: int = None
|
||||
icon: str = None
|
||||
services: list[str] = field(default_factory=list)
|
||||
config_services: list[str] = field(default_factory=list)
|
||||
services: List[str] = field(default_factory=list)
|
||||
config_services: List[str] = field(default_factory=list)
|
||||
x: float = None
|
||||
y: float = None
|
||||
lat: float = None
|
||||
|
|
@ -91,11 +91,6 @@ class NodeOptions:
|
|||
server: str = None
|
||||
image: str = None
|
||||
emane: str = None
|
||||
legacy: bool = False
|
||||
# src, dst
|
||||
binds: list[tuple[str, str]] = field(default_factory=list)
|
||||
# src, dst, unique, delete
|
||||
volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
|
|
@ -146,9 +141,8 @@ class InterfaceData:
|
|||
ip4_mask: int = None
|
||||
ip6: str = None
|
||||
ip6_mask: int = None
|
||||
mtu: int = None
|
||||
|
||||
def get_ips(self) -> list[str]:
|
||||
def get_ips(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of ip4 and ip6 addresses when present.
|
||||
|
||||
|
|
@ -178,68 +172,6 @@ class LinkOptions:
|
|||
mburst: int = None
|
||||
unidirectional: int = None
|
||||
key: int = None
|
||||
buffer: int = None
|
||||
|
||||
def update(self, options: "LinkOptions") -> bool:
|
||||
"""
|
||||
Updates current options with values from other options.
|
||||
|
||||
:param options: options to update with
|
||||
:return: True if any value has changed, False otherwise
|
||||
"""
|
||||
changed = False
|
||||
if options.delay is not None and 0 <= options.delay != self.delay:
|
||||
self.delay = options.delay
|
||||
changed = True
|
||||
if options.bandwidth is not None and 0 <= options.bandwidth != self.bandwidth:
|
||||
self.bandwidth = options.bandwidth
|
||||
changed = True
|
||||
if options.loss is not None and 0 <= options.loss != self.loss:
|
||||
self.loss = options.loss
|
||||
changed = True
|
||||
if options.dup is not None and 0 <= options.dup != self.dup:
|
||||
self.dup = options.dup
|
||||
changed = True
|
||||
if options.jitter is not None and 0 <= options.jitter != self.jitter:
|
||||
self.jitter = options.jitter
|
||||
changed = True
|
||||
if options.buffer is not None and 0 <= options.buffer != self.buffer:
|
||||
self.buffer = options.buffer
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def is_clear(self) -> bool:
|
||||
"""
|
||||
Checks if the current option values represent a clear state.
|
||||
|
||||
:return: True if the current values should clear, False otherwise
|
||||
"""
|
||||
clear = self.delay is None or self.delay <= 0
|
||||
clear &= self.jitter is None or self.jitter <= 0
|
||||
clear &= self.loss is None or self.loss <= 0
|
||||
clear &= self.dup is None or self.dup <= 0
|
||||
clear &= self.bandwidth is None or self.bandwidth <= 0
|
||||
clear &= self.buffer is None or self.buffer <= 0
|
||||
return clear
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
Custom logic to check if this link options is equivalent to another.
|
||||
|
||||
:param other: other object to check
|
||||
:return: True if they are both link options with the same values,
|
||||
False otherwise
|
||||
"""
|
||||
if not isinstance(other, LinkOptions):
|
||||
return False
|
||||
return (
|
||||
self.delay == other.delay
|
||||
and self.jitter == other.jitter
|
||||
and self.loss == other.loss
|
||||
and self.dup == other.dup
|
||||
and self.bandwidth == other.bandwidth
|
||||
and self.buffer == other.buffer
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -6,23 +6,19 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Tuple
|
||||
|
||||
import netaddr
|
||||
from fabric import Connection
|
||||
from invoke import UnexpectedExit
|
||||
|
||||
from core import utils
|
||||
from core.emulator.links import CoreLink
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import get_requirements
|
||||
from core.nodes.interface import GreTap
|
||||
from core.nodes.network import CoreNetwork, CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
|
@ -48,7 +44,7 @@ class DistributedServer:
|
|||
self.lock: threading.Lock = threading.Lock()
|
||||
|
||||
def remote_cmd(
|
||||
self, cmd: str, env: dict[str, str] = None, cwd: str = None, wait: bool = True
|
||||
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Run command remotely using server connection.
|
||||
|
|
@ -65,7 +61,7 @@ class DistributedServer:
|
|||
replace_env = env is not None
|
||||
if not wait:
|
||||
cmd += " &"
|
||||
logger.debug(
|
||||
logging.debug(
|
||||
"remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd
|
||||
)
|
||||
try:
|
||||
|
|
@ -83,31 +79,31 @@ class DistributedServer:
|
|||
stdout, stderr = e.streams_for_display()
|
||||
raise CoreCommandError(e.result.exited, cmd, stdout, stderr)
|
||||
|
||||
def remote_put(self, src_path: Path, dst_path: Path) -> None:
|
||||
def remote_put(self, source: str, destination: str) -> None:
|
||||
"""
|
||||
Push file to remote server.
|
||||
|
||||
:param src_path: source file to push
|
||||
:param dst_path: destination file location
|
||||
:param source: source file to push
|
||||
:param destination: destination file location
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
self.conn.put(str(src_path), str(dst_path))
|
||||
self.conn.put(source, destination)
|
||||
|
||||
def remote_put_temp(self, dst_path: Path, data: str) -> None:
|
||||
def remote_put_temp(self, destination: str, data: str) -> None:
|
||||
"""
|
||||
Remote push file contents to a remote server, using a temp file as an
|
||||
intermediate step.
|
||||
|
||||
:param dst_path: file destination for data
|
||||
:param destination: file destination for data
|
||||
:param data: data to store in remote file
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
temp.write(data.encode())
|
||||
temp.write(data.encode("utf-8"))
|
||||
temp.close()
|
||||
self.conn.put(temp.name, str(dst_path))
|
||||
self.conn.put(temp.name, destination)
|
||||
os.unlink(temp.name)
|
||||
|
||||
|
||||
|
|
@ -123,9 +119,11 @@ class DistributedController:
|
|||
:param session: session
|
||||
"""
|
||||
self.session: "Session" = session
|
||||
self.servers: dict[str, DistributedServer] = OrderedDict()
|
||||
self.tunnels: dict[int, tuple[GreTap, GreTap]] = {}
|
||||
self.address: str = self.session.options.get("distributed_address")
|
||||
self.servers: Dict[str, DistributedServer] = OrderedDict()
|
||||
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
|
||||
self.address: str = self.session.options.get_config(
|
||||
"distributed_address", default=None
|
||||
)
|
||||
|
||||
def add_server(self, name: str, host: str) -> None:
|
||||
"""
|
||||
|
|
@ -146,7 +144,7 @@ class DistributedController:
|
|||
f"command({requirement})"
|
||||
)
|
||||
self.servers[name] = server
|
||||
cmd = f"mkdir -p {self.session.directory}"
|
||||
cmd = f"mkdir -p {self.session.session_dir}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
def execute(self, func: Callable[[DistributedServer], None]) -> None:
|
||||
|
|
@ -172,55 +170,41 @@ class DistributedController:
|
|||
tunnels = self.tunnels[key]
|
||||
for tunnel in tunnels:
|
||||
tunnel.shutdown()
|
||||
|
||||
# remove all remote session directories
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
cmd = f"rm -rf {self.session.directory}"
|
||||
cmd = f"rm -rf {self.session.session_dir}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
# clear tunnels
|
||||
self.tunnels.clear()
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Start distributed network tunnels for control networks.
|
||||
Start distributed network tunnels.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
for node in self.session.nodes.values():
|
||||
if not isinstance(node, CtrlNet) or node.serverintf is not None:
|
||||
for node_id in self.session.nodes:
|
||||
node = self.session.nodes[node_id]
|
||||
if not isinstance(node, CoreNetwork):
|
||||
continue
|
||||
if isinstance(node, CtrlNet) and node.serverintf is not None:
|
||||
continue
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
self.create_gre_tunnel(node, server, mtu, True)
|
||||
|
||||
def create_gre_tunnels(self, core_link: CoreLink) -> None:
|
||||
"""
|
||||
Creates gre tunnels for a core link with a ptp network connection.
|
||||
|
||||
:param core_link: core link to create gre tunnel for
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.servers:
|
||||
return
|
||||
if not core_link.ptp:
|
||||
raise CoreError(
|
||||
"attempted to create gre tunnel for core link without a ptp network"
|
||||
)
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
for server in self.servers.values():
|
||||
self.create_gre_tunnel(core_link.ptp, server, mtu, True)
|
||||
self.create_gre_tunnel(node, server)
|
||||
|
||||
def create_gre_tunnel(
|
||||
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
|
||||
) -> tuple[GreTap, GreTap]:
|
||||
self, node: CoreNetwork, server: DistributedServer
|
||||
) -> Tuple[GreTap, GreTap]:
|
||||
"""
|
||||
Create gre tunnel using a pair of gre taps between the local and remote server.
|
||||
|
||||
:param node: node to create gre tunnel for
|
||||
:param server: server to create tunnel for
|
||||
:param mtu: mtu for gre taps
|
||||
:param start: True to start gre taps, False otherwise
|
||||
:param server: server to create
|
||||
tunnel for
|
||||
:return: local and remote gre taps created for tunnel
|
||||
"""
|
||||
host = server.host
|
||||
|
|
@ -228,20 +212,23 @@ class DistributedController:
|
|||
tunnel = self.tunnels.get(key)
|
||||
if tunnel is not None:
|
||||
return tunnel
|
||||
|
||||
# local to server
|
||||
logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key)
|
||||
local_tap = GreTap(self.session, host, key=key, mtu=mtu)
|
||||
if start:
|
||||
local_tap.startup()
|
||||
local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||
logging.info(
|
||||
"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.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||
|
||||
# server to local
|
||||
logger.info(
|
||||
logging.info(
|
||||
"remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key
|
||||
)
|
||||
remote_tap = GreTap(self.session, self.address, key=key, server=server, mtu=mtu)
|
||||
if start:
|
||||
remote_tap.startup()
|
||||
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||
remote_tap = GreTap(
|
||||
session=self.session, remoteip=self.address, key=key, server=server
|
||||
)
|
||||
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||
|
||||
# save tunnels for shutdown
|
||||
tunnel = (local_tap, remote_tap)
|
||||
self.tunnels[key] = tunnel
|
||||
|
|
@ -257,7 +244,7 @@ class DistributedController:
|
|||
:param node2_id: node two id
|
||||
:return: tunnel key for the node pair
|
||||
"""
|
||||
logger.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||
logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||
key = (
|
||||
(self.session.id << 16)
|
||||
^ utils.hashkey(node1_id)
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@ class MessageFlags(Enum):
|
|||
TTY = 0x40
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class NodeTypes(Enum):
|
||||
"""
|
||||
Node types.
|
||||
|
|
@ -49,8 +38,6 @@ class NodeTypes(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
WIRELESS = 17
|
||||
PODMAN = 18
|
||||
|
||||
|
||||
class LinkTypes(Enum):
|
||||
|
|
@ -119,9 +106,6 @@ class EventTypes(Enum):
|
|||
def should_start(self) -> bool:
|
||||
return self.value > self.DEFINITION_STATE.value
|
||||
|
||||
def already_collected(self) -> bool:
|
||||
return self.value >= self.DATACOLLECT_STATE.value
|
||||
|
||||
|
||||
class ExceptionLevels(Enum):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
import logging
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from core.emulator.enumerations import EventTypes
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HookManager:
|
||||
"""
|
||||
Provides functionality for managing and running script/callback hooks.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a HookManager instance.
|
||||
"""
|
||||
self.script_hooks: dict[EventTypes, dict[str, str]] = {}
|
||||
self.callback_hooks: dict[EventTypes, list[Callable[[], None]]] = {}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Clear all current hooks.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.script_hooks.clear()
|
||||
self.callback_hooks.clear()
|
||||
|
||||
def add_script_hook(self, state: EventTypes, file_name: str, data: str) -> None:
|
||||
"""
|
||||
Add a hook script to run for a given state.
|
||||
|
||||
:param state: state to run hook on
|
||||
:param file_name: hook file name
|
||||
:param data: file data
|
||||
:return: nothing
|
||||
"""
|
||||
logger.info("setting state hook: %s - %s", state, file_name)
|
||||
state_hooks = self.script_hooks.setdefault(state, {})
|
||||
if file_name in state_hooks:
|
||||
raise CoreError(
|
||||
f"adding duplicate state({state.name}) hook script({file_name})"
|
||||
)
|
||||
state_hooks[file_name] = data
|
||||
|
||||
def delete_script_hook(self, state: EventTypes, file_name: str) -> None:
|
||||
"""
|
||||
Delete a script hook from a given state.
|
||||
|
||||
:param state: state to delete script hook from
|
||||
:param file_name: name of script to delete
|
||||
:return: nothing
|
||||
"""
|
||||
state_hooks = self.script_hooks.get(state, {})
|
||||
if file_name not in state_hooks:
|
||||
raise CoreError(
|
||||
f"deleting state({state.name}) hook script({file_name}) "
|
||||
"that does not exist"
|
||||
)
|
||||
del state_hooks[file_name]
|
||||
|
||||
def add_callback_hook(
|
||||
self, state: EventTypes, hook: Callable[[EventTypes], None]
|
||||
) -> None:
|
||||
"""
|
||||
Add a hook callback to run for a state.
|
||||
|
||||
:param state: state to add hook for
|
||||
:param hook: callback to run
|
||||
:return: nothing
|
||||
"""
|
||||
hooks = self.callback_hooks.setdefault(state, [])
|
||||
if hook in hooks:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"adding duplicate state({state.name}) hook callback({name})"
|
||||
)
|
||||
hooks.append(hook)
|
||||
|
||||
def delete_callback_hook(
|
||||
self, state: EventTypes, hook: Callable[[EventTypes], None]
|
||||
) -> None:
|
||||
"""
|
||||
Delete a state hook.
|
||||
|
||||
:param state: state to delete hook for
|
||||
:param hook: hook to delete
|
||||
:return: nothing
|
||||
"""
|
||||
hooks = self.callback_hooks.get(state, [])
|
||||
if hook not in hooks:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"deleting state({state.name}) hook callback({name}) "
|
||||
"that does not exist"
|
||||
)
|
||||
hooks.remove(hook)
|
||||
|
||||
def run_hooks(
|
||||
self, state: EventTypes, directory: Path, env: dict[str, str]
|
||||
) -> None:
|
||||
"""
|
||||
Run all hooks for the current state.
|
||||
|
||||
:param state: state to run hooks for
|
||||
:param directory: directory to run script hooks within
|
||||
:param env: environment to run script hooks with
|
||||
:return: nothing
|
||||
"""
|
||||
for state_hooks in self.script_hooks.get(state, {}):
|
||||
for file_name, data in state_hooks.items():
|
||||
logger.info("running hook %s", file_name)
|
||||
file_path = directory / file_name
|
||||
log_path = directory / f"{file_name}.log"
|
||||
try:
|
||||
with file_path.open("w") as f:
|
||||
f.write(data)
|
||||
with log_path.open("w") as f:
|
||||
args = ["/bin/sh", file_name]
|
||||
subprocess.check_call(
|
||||
args,
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
cwd=directory,
|
||||
env=env,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
raise CoreError(
|
||||
f"failure running state({state.name}) "
|
||||
f"hook script({file_name}): {e}"
|
||||
)
|
||||
for hook in self.callback_hooks.get(state, []):
|
||||
try:
|
||||
hook()
|
||||
except Exception as e:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"failure running state({state.name}) "
|
||||
f"hook callback({name}): {e}"
|
||||
)
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
"""
|
||||
Provides functionality for maintaining information about known links
|
||||
for a session.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import ValuesView
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from core.emulator.data import LinkData, LinkOptions
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import PtpNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LinkKeyType = tuple[int, Optional[int], int, Optional[int]]
|
||||
|
||||
|
||||
def create_key(
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> LinkKeyType:
|
||||
"""
|
||||
Creates a unique key for tracking links.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:return: link key
|
||||
"""
|
||||
iface1_id = iface1.id if iface1 else None
|
||||
iface2_id = iface2.id if iface2 else None
|
||||
if node1.id < node2.id:
|
||||
return node1.id, iface1_id, node2.id, iface2_id
|
||||
else:
|
||||
return node2.id, iface2_id, node1.id, iface1_id
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoreLink:
|
||||
"""
|
||||
Provides a core link data structure.
|
||||
"""
|
||||
|
||||
node1: NodeBase
|
||||
iface1: Optional[CoreInterface]
|
||||
node2: NodeBase
|
||||
iface2: Optional[CoreInterface]
|
||||
ptp: PtpNet = None
|
||||
label: str = None
|
||||
color: str = None
|
||||
|
||||
def key(self) -> LinkKeyType:
|
||||
"""
|
||||
Retrieve the key for this link.
|
||||
|
||||
:return: link key
|
||||
"""
|
||||
return create_key(self.node1, self.iface1, self.node2, self.iface2)
|
||||
|
||||
def is_unidirectional(self) -> bool:
|
||||
"""
|
||||
Checks if this link is considered unidirectional, due to current
|
||||
iface configurations.
|
||||
|
||||
:return: True if unidirectional, False otherwise
|
||||
"""
|
||||
unidirectional = False
|
||||
if self.iface1 and self.iface2:
|
||||
unidirectional = self.iface1.options != self.iface2.options
|
||||
return unidirectional
|
||||
|
||||
def options(self) -> LinkOptions:
|
||||
"""
|
||||
Retrieve the options for this link.
|
||||
|
||||
:return: options for this link
|
||||
"""
|
||||
if self.is_unidirectional():
|
||||
options = self.iface1.options
|
||||
else:
|
||||
if self.iface1:
|
||||
options = self.iface1.options
|
||||
else:
|
||||
options = self.iface2.options
|
||||
return options
|
||||
|
||||
def get_data(self, message_type: MessageFlags, source: str = None) -> LinkData:
|
||||
"""
|
||||
Create link data for this link.
|
||||
|
||||
:param message_type: link data message type
|
||||
:param source: source for this data
|
||||
:return: link data
|
||||
"""
|
||||
iface1_data = self.iface1.get_data() if self.iface1 else None
|
||||
iface2_data = self.iface2.get_data() if self.iface2 else None
|
||||
return LinkData(
|
||||
message_type=message_type,
|
||||
type=LinkTypes.WIRED,
|
||||
node1_id=self.node1.id,
|
||||
node2_id=self.node2.id,
|
||||
iface1=iface1_data,
|
||||
iface2=iface2_data,
|
||||
options=self.options(),
|
||||
label=self.label,
|
||||
color=self.color,
|
||||
source=source,
|
||||
)
|
||||
|
||||
def get_data_unidirectional(self, source: str = None) -> LinkData:
|
||||
"""
|
||||
Create other unidirectional link data.
|
||||
|
||||
:param source: source for this data
|
||||
:return: unidirectional link data
|
||||
"""
|
||||
iface1_data = self.iface1.get_data() if self.iface1 else None
|
||||
iface2_data = self.iface2.get_data() if self.iface2 else None
|
||||
return LinkData(
|
||||
message_type=MessageFlags.NONE,
|
||||
type=LinkTypes.WIRED,
|
||||
node1_id=self.node2.id,
|
||||
node2_id=self.node1.id,
|
||||
iface1=iface2_data,
|
||||
iface2=iface1_data,
|
||||
options=self.iface2.options,
|
||||
label=self.label,
|
||||
color=self.color,
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
class LinkManager:
|
||||
"""
|
||||
Provides core link management.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a LinkManager instance.
|
||||
"""
|
||||
self._links: dict[LinkKeyType, CoreLink] = {}
|
||||
self._node_links: dict[int, dict[LinkKeyType, CoreLink]] = {}
|
||||
|
||||
def add(self, core_link: CoreLink) -> None:
|
||||
"""
|
||||
Add a core link to be tracked.
|
||||
|
||||
:param core_link: link to track
|
||||
:return: nothing
|
||||
"""
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
if core_link.key() in self._links:
|
||||
raise CoreError(
|
||||
f"node1({node1.name}) iface1({iface1.id}) "
|
||||
f"node2({node2.name}) iface2({iface2.id}) link already exists"
|
||||
)
|
||||
logger.info(
|
||||
"adding link from node(%s:%s) to node(%s:%s)",
|
||||
node1.name,
|
||||
iface1.name if iface1 else None,
|
||||
node2.name,
|
||||
iface2.name if iface2 else None,
|
||||
)
|
||||
self._links[core_link.key()] = core_link
|
||||
node1_links = self._node_links.setdefault(node1.id, {})
|
||||
node1_links[core_link.key()] = core_link
|
||||
node2_links = self._node_links.setdefault(node2.id, {})
|
||||
node2_links[core_link.key()] = core_link
|
||||
|
||||
def delete(
|
||||
self,
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> CoreLink:
|
||||
"""
|
||||
Remove a link from being tracked.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:return: removed core link
|
||||
"""
|
||||
key = create_key(node1, iface1, node2, iface2)
|
||||
if key not in self._links:
|
||||
raise CoreError(
|
||||
f"node1({node1.name}) iface1({iface1.id}) "
|
||||
f"node2({node2.name}) iface2({iface2.id}) is not linked"
|
||||
)
|
||||
logger.info(
|
||||
"deleting link from node(%s:%s) to node(%s:%s)",
|
||||
node1.name,
|
||||
iface1.name if iface1 else None,
|
||||
node2.name,
|
||||
iface2.name if iface2 else None,
|
||||
)
|
||||
node1_links = self._node_links[node1.id]
|
||||
node1_links.pop(key)
|
||||
node2_links = self._node_links[node2.id]
|
||||
node2_links.pop(key)
|
||||
return self._links.pop(key)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Resets and clears all tracking information.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self._links.clear()
|
||||
self._node_links.clear()
|
||||
|
||||
def get_link(
|
||||
self,
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> Optional[CoreLink]:
|
||||
"""
|
||||
Retrieve a link for provided values.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: interface for node1
|
||||
:param node2: second node in link
|
||||
:param iface2: interface for node2
|
||||
:return: core link if present, None otherwise
|
||||
"""
|
||||
key = create_key(node1, iface1, node2, iface2)
|
||||
return self._links.get(key)
|
||||
|
||||
def links(self) -> ValuesView[CoreLink]:
|
||||
"""
|
||||
Retrieve all known links
|
||||
|
||||
:return: iterator for all known links
|
||||
"""
|
||||
return self._links.values()
|
||||
|
||||
def node_links(self, node: NodeBase) -> ValuesView[CoreLink]:
|
||||
"""
|
||||
Retrieve all links for a given node.
|
||||
|
||||
:param node: node to get links for
|
||||
:return: node links
|
||||
"""
|
||||
return self._node_links.get(node.id, {}).values()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,87 +1,93 @@
|
|||
from typing import Optional
|
||||
from typing import Any, List
|
||||
|
||||
from core.config import ConfigBool, ConfigInt, ConfigString, Configuration
|
||||
from core.errors import CoreError
|
||||
from core.config import ConfigurableManager, ConfigurableOptions, Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
|
||||
from core.plugins.sdt import Sdt
|
||||
|
||||
|
||||
class SessionConfig:
|
||||
class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
||||
"""
|
||||
Provides session configuration.
|
||||
"""
|
||||
|
||||
options: list[Configuration] = [
|
||||
ConfigString(id="controlnet", label="Control Network"),
|
||||
ConfigString(id="controlnet0", label="Control Network 0"),
|
||||
ConfigString(id="controlnet1", label="Control Network 1"),
|
||||
ConfigString(id="controlnet2", label="Control Network 2"),
|
||||
ConfigString(id="controlnet3", label="Control Network 3"),
|
||||
ConfigString(id="controlnet_updown_script", label="Control Network Script"),
|
||||
ConfigBool(id="enablerj45", default="1", label="Enable RJ45s"),
|
||||
ConfigBool(id="preservedir", default="0", label="Preserve session dir"),
|
||||
ConfigBool(id="enablesdt", default="0", label="Enable SDT3D output"),
|
||||
ConfigString(id="sdturl", default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL"),
|
||||
ConfigBool(id="ovs", default="0", label="Enable OVS"),
|
||||
ConfigInt(id="platform_id_start", default="1", label="EMANE Platform ID Start"),
|
||||
ConfigInt(id="nem_id_start", default="1", label="EMANE NEM ID Start"),
|
||||
ConfigBool(id="link_enabled", default="1", label="EMANE Links?"),
|
||||
ConfigInt(
|
||||
id="loss_threshold", default="30", label="EMANE Link Loss Threshold (%)"
|
||||
name: str = "session"
|
||||
options: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
|
||||
),
|
||||
ConfigInt(
|
||||
id="link_interval", default="1", label="EMANE Link Check Interval (sec)"
|
||||
Configuration(
|
||||
_id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet_updown_script",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Control Network Script",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablerj45",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
label="Enable RJ45s",
|
||||
),
|
||||
Configuration(
|
||||
_id="preservedir",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Preserve session dir",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablesdt",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Enable SDT3D output",
|
||||
),
|
||||
Configuration(
|
||||
_id="sdturl",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
default=Sdt.DEFAULT_SDT_URL,
|
||||
label="SDT3D URL",
|
||||
),
|
||||
Configuration(
|
||||
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
|
||||
),
|
||||
ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"),
|
||||
ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
|
||||
]
|
||||
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
||||
|
||||
def __init__(self, config: dict[str, str] = None) -> None:
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.set_configs(self.default_values())
|
||||
|
||||
def get_config(
|
||||
self,
|
||||
_id: str,
|
||||
node_id: int = ConfigurableManager._default_node,
|
||||
config_type: str = ConfigurableManager._default_type,
|
||||
default: Any = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a SessionConfig instance.
|
||||
Retrieves a specific configuration for a node and configuration type.
|
||||
|
||||
:param config: configuration to initialize with
|
||||
:param _id: specific configuration to retrieve
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:param default: default value to return when value is not found
|
||||
:return: configuration value
|
||||
"""
|
||||
self._config: dict[str, str] = {x.id: x.default for x in self.options}
|
||||
self._config.update(config or {})
|
||||
value = super().get_config(_id, node_id, config_type, default)
|
||||
if value == "":
|
||||
value = default
|
||||
return value
|
||||
|
||||
def update(self, config: dict[str, str]) -> None:
|
||||
"""
|
||||
Update current configuration with provided values.
|
||||
|
||||
:param config: configuration to update with
|
||||
:return: nothing
|
||||
"""
|
||||
self._config.update(config)
|
||||
|
||||
def set(self, name: str, value: str) -> None:
|
||||
"""
|
||||
Set a configuration value.
|
||||
|
||||
:param name: name of configuration to set
|
||||
:param value: value to set
|
||||
:return: nothing
|
||||
"""
|
||||
self._config[name] = value
|
||||
|
||||
def get(self, name: str, default: str = None) -> Optional[str]:
|
||||
"""
|
||||
Retrieve configuration value.
|
||||
|
||||
:param name: name of configuration to get
|
||||
:param default: value to return as default
|
||||
:return: return found configuration value or default
|
||||
"""
|
||||
return self._config.get(name, default)
|
||||
|
||||
def all(self) -> dict[str, str]:
|
||||
"""
|
||||
Retrieve all configuration options.
|
||||
|
||||
:return: configuration value dict
|
||||
"""
|
||||
return self._config
|
||||
|
||||
def get_bool(self, name: str, default: bool = None) -> bool:
|
||||
def get_config_bool(self, name: str, default: Any = None) -> bool:
|
||||
"""
|
||||
Get configuration value as a boolean.
|
||||
|
||||
|
|
@ -89,15 +95,12 @@ class SessionConfig:
|
|||
:param default: default value if not found
|
||||
:return: boolean for configuration value
|
||||
"""
|
||||
value = self._config.get(name)
|
||||
if value is None and default is None:
|
||||
raise CoreError(f"missing session options for {name}")
|
||||
value = self.get_config(name)
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return value.lower() == "true"
|
||||
return value.lower() == "true"
|
||||
|
||||
def get_int(self, name: str, default: int = None) -> int:
|
||||
def get_config_int(self, name: str, default: Any = None) -> int:
|
||||
"""
|
||||
Get configuration value as int.
|
||||
|
||||
|
|
@ -105,10 +108,7 @@ class SessionConfig:
|
|||
:param default: default value if not found
|
||||
:return: int for configuration value
|
||||
"""
|
||||
value = self._config.get(name)
|
||||
if value is None and default is None:
|
||||
raise CoreError(f"missing session options for {name}")
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return int(value)
|
||||
value = self.get_config(name, default=default)
|
||||
if value is not None:
|
||||
value = int(value)
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class CoreCommandError(subprocess.CalledProcessError):
|
|||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"command({self.cmd}), status({self.returncode}):\n"
|
||||
f"Command({self.cmd}), Status({self.returncode}):\n"
|
||||
f"stdout: {self.output}\nstderr: {self.stderr}"
|
||||
)
|
||||
|
||||
|
|
@ -30,27 +30,3 @@ class CoreXmlError(Exception):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreServiceError(Exception):
|
||||
"""
|
||||
Used when there is an error related to accessing a service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreServiceBootError(Exception):
|
||||
"""
|
||||
Used when there is an error booting a service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreConfigError(Exception):
|
||||
"""
|
||||
Used when there is an error defining a configurable option.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,33 +1,22 @@
|
|||
BASH: str = "bash"
|
||||
ETHTOOL: str = "ethtool"
|
||||
IP: str = "ip"
|
||||
MOUNT: str = "mount"
|
||||
NFTABLES: str = "nft"
|
||||
OVS_VSCTL: str = "ovs-vsctl"
|
||||
SYSCTL: str = "sysctl"
|
||||
TC: str = "tc"
|
||||
TEST: str = "test"
|
||||
UMOUNT: str = "umount"
|
||||
VCMD: str = "vcmd"
|
||||
from typing import List
|
||||
|
||||
VNODED: str = "vnoded"
|
||||
VCMD: str = "vcmd"
|
||||
SYSCTL: str = "sysctl"
|
||||
IP: str = "ip"
|
||||
ETHTOOL: str = "ethtool"
|
||||
TC: str = "tc"
|
||||
EBTABLES: str = "ebtables"
|
||||
MOUNT: str = "mount"
|
||||
UMOUNT: str = "umount"
|
||||
OVS_VSCTL: str = "ovs-vsctl"
|
||||
|
||||
COMMON_REQUIREMENTS: list[str] = [
|
||||
BASH,
|
||||
ETHTOOL,
|
||||
IP,
|
||||
MOUNT,
|
||||
NFTABLES,
|
||||
SYSCTL,
|
||||
TC,
|
||||
TEST,
|
||||
UMOUNT,
|
||||
VCMD,
|
||||
VNODED,
|
||||
]
|
||||
OVS_REQUIREMENTS: list[str] = [OVS_VSCTL]
|
||||
COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT]
|
||||
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
|
||||
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
|
||||
|
||||
|
||||
def get_requirements(use_ovs: bool) -> list[str]:
|
||||
def get_requirements(use_ovs: bool) -> List[str]:
|
||||
"""
|
||||
Retrieve executable requirements needed to run CORE.
|
||||
|
||||
|
|
@ -37,4 +26,6 @@ def get_requirements(use_ovs: bool) -> list[str]:
|
|||
requirements = COMMON_REQUIREMENTS
|
||||
if use_ovs:
|
||||
requirements += OVS_REQUIREMENTS
|
||||
else:
|
||||
requirements += VCMD_REQUIREMENTS
|
||||
return requirements
|
||||
|
|
|
|||
|
|
@ -1,43 +1,41 @@
|
|||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import PhotoImage, font, messagebox, ttk
|
||||
from tkinter import PhotoImage, font, ttk
|
||||
from tkinter.ttk import Progressbar
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui import appconfig, images
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui import themes
|
||||
from core.gui import appconfig, themes
|
||||
from core.gui.appconfig import GuiConfig
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.dialogs.error import ErrorDialog
|
||||
from core.gui.frames.base import InfoFrameBase
|
||||
from core.gui.frames.default import DefaultInfoFrame
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.menubar import Menubar
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.statusbar import StatusBar
|
||||
from core.gui.themes import PADY
|
||||
from core.gui.toolbar import Toolbar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WIDTH: int = 1000
|
||||
HEIGHT: int = 800
|
||||
|
||||
|
||||
class Application(ttk.Frame):
|
||||
def __init__(self, proxy: bool, session_id: int = None) -> None:
|
||||
def __init__(self, proxy: bool) -> None:
|
||||
super().__init__()
|
||||
# load node icons
|
||||
nutils.setup()
|
||||
NodeUtils.setup()
|
||||
|
||||
# widgets
|
||||
self.menubar: Optional[Menubar] = None
|
||||
self.toolbar: Optional[Toolbar] = None
|
||||
self.right_frame: Optional[ttk.Frame] = None
|
||||
self.manager: Optional[CanvasManager] = None
|
||||
self.canvas: Optional[CanvasGraph] = None
|
||||
self.statusbar: Optional[StatusBar] = None
|
||||
self.progress: Optional[Progressbar] = None
|
||||
self.infobar: Optional[ttk.Frame] = None
|
||||
|
|
@ -45,7 +43,7 @@ class Application(ttk.Frame):
|
|||
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||
|
||||
# fonts
|
||||
self.fonts_size: dict[str, int] = {}
|
||||
self.fonts_size: Dict[str, int] = {}
|
||||
self.icon_text_font: Optional[font.Font] = None
|
||||
self.edge_font: Optional[font.Font] = None
|
||||
|
||||
|
|
@ -58,7 +56,7 @@ class Application(ttk.Frame):
|
|||
self.core: CoreClient = CoreClient(self, proxy)
|
||||
self.setup_app()
|
||||
self.draw()
|
||||
self.core.setup(session_id)
|
||||
self.core.setup()
|
||||
|
||||
def setup_scaling(self) -> None:
|
||||
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
||||
|
|
@ -79,7 +77,7 @@ class Application(ttk.Frame):
|
|||
self.master.title("CORE")
|
||||
self.center()
|
||||
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
self.master.tk.call("wm", "iconphoto", self.master._w, image)
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.setup_file_dialogs()
|
||||
|
|
@ -113,13 +111,13 @@ class Application(ttk.Frame):
|
|||
self.master.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.columnconfigure(1, weight=1)
|
||||
self.grid(sticky=tk.NSEW)
|
||||
self.grid(sticky="nsew")
|
||||
self.toolbar = Toolbar(self)
|
||||
self.toolbar.grid(sticky=tk.NS)
|
||||
self.toolbar.grid(sticky="ns")
|
||||
self.right_frame = ttk.Frame(self)
|
||||
self.right_frame.columnconfigure(0, weight=1)
|
||||
self.right_frame.rowconfigure(0, weight=1)
|
||||
self.right_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
self.right_frame.grid(row=0, column=1, sticky="nsew")
|
||||
self.draw_canvas()
|
||||
self.draw_infobar()
|
||||
self.draw_status()
|
||||
|
|
@ -138,20 +136,32 @@ class Application(ttk.Frame):
|
|||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
def draw_canvas(self) -> None:
|
||||
self.manager = CanvasManager(self.right_frame, self, self.core)
|
||||
self.manager.notebook.grid(sticky=tk.NSEW)
|
||||
canvas_frame = ttk.Frame(self.right_frame)
|
||||
canvas_frame.rowconfigure(0, weight=1)
|
||||
canvas_frame.columnconfigure(0, weight=1)
|
||||
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
|
||||
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
||||
self.canvas.grid(sticky="nsew")
|
||||
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
||||
scroll_y.grid(row=0, column=1, sticky="ns")
|
||||
scroll_x = ttk.Scrollbar(
|
||||
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.grid(row=1, column=0, sticky="ew")
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
def draw_status(self) -> None:
|
||||
self.statusbar = StatusBar(self.right_frame, self)
|
||||
self.statusbar.grid(sticky=tk.EW, columnspan=2)
|
||||
self.statusbar.grid(sticky="ew", columnspan=2)
|
||||
|
||||
def display_info(self, frame_class: type[InfoFrameBase], **kwargs: Any) -> None:
|
||||
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
|
||||
if not self.show_infobar.get():
|
||||
return
|
||||
self.clear_info()
|
||||
self.info_frame = frame_class(self.infobar, **kwargs)
|
||||
self.info_frame.draw()
|
||||
self.info_frame.grid(sticky=tk.NSEW)
|
||||
self.info_frame.grid(sticky="nsew")
|
||||
|
||||
def clear_info(self) -> None:
|
||||
if self.info_frame:
|
||||
|
|
@ -164,35 +174,22 @@ class Application(ttk.Frame):
|
|||
|
||||
def show_info(self) -> None:
|
||||
self.default_info()
|
||||
self.infobar.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
self.infobar.grid(row=0, column=1, sticky="nsew")
|
||||
|
||||
def hide_info(self) -> None:
|
||||
self.infobar.grid_forget()
|
||||
|
||||
def show_grpc_exception(
|
||||
self, message: str, e: grpc.RpcError, blocking: bool = False
|
||||
) -> None:
|
||||
logger.exception("app grpc exception", exc_info=e)
|
||||
dialog = ErrorDialog(self, "GRPC Exception", message, e.details())
|
||||
if blocking:
|
||||
dialog.show()
|
||||
else:
|
||||
self.after(0, lambda: dialog.show())
|
||||
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
|
||||
logging.exception("app grpc exception", exc_info=e)
|
||||
message = e.details()
|
||||
self.show_error(title, message)
|
||||
|
||||
def show_exception(self, message: str, e: Exception) -> None:
|
||||
logger.exception("app exception", exc_info=e)
|
||||
self.after(
|
||||
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
|
||||
)
|
||||
def show_exception(self, title: str, e: Exception) -> None:
|
||||
logging.exception("app exception", exc_info=e)
|
||||
self.show_error(title, str(e))
|
||||
|
||||
def show_exception_data(self, title: str, message: str, details: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message, details).show())
|
||||
|
||||
def show_error(self, title: str, message: str, blocking: bool = False) -> None:
|
||||
if blocking:
|
||||
messagebox.showerror(title, message, parent=self)
|
||||
else:
|
||||
self.after(0, lambda: messagebox.showerror(title, message, parent=self))
|
||||
def show_error(self, title: str, message: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message).show())
|
||||
|
||||
def on_closing(self) -> None:
|
||||
if self.toolbar.picker:
|
||||
|
|
@ -204,17 +201,15 @@ class Application(ttk.Frame):
|
|||
|
||||
def joined_session_update(self) -> None:
|
||||
if self.core.is_runtime():
|
||||
self.menubar.set_state(is_runtime=True)
|
||||
self.toolbar.set_runtime()
|
||||
else:
|
||||
self.menubar.set_state(is_runtime=False)
|
||||
self.toolbar.set_design()
|
||||
|
||||
def get_enum_icon(self, image_enum: ImageEnum, *, width: int) -> PhotoImage:
|
||||
return images.from_enum(image_enum, width=width, scale=self.app_scale)
|
||||
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
|
||||
return Images.get(image_enum, int(width * self.app_scale))
|
||||
|
||||
def get_file_icon(self, file_path: str, *, width: int) -> PhotoImage:
|
||||
return images.from_file(file_path, width=width, scale=self.app_scale)
|
||||
def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
|
||||
return Images.get_custom(image_file, int(width * self.app_scale))
|
||||
|
||||
def close(self) -> None:
|
||||
self.master.destroy()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
import yaml
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
|
|||
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
|
||||
|
||||
# configuration data
|
||||
TERMINALS: dict[str, str] = {
|
||||
TERMINALS: Dict[str, str] = {
|
||||
"xterm": "xterm -e",
|
||||
"aterm": "aterm -e",
|
||||
"eterm": "eterm -e",
|
||||
|
|
@ -36,7 +36,7 @@ TERMINALS: dict[str, str] = {
|
|||
"xfce4-terminal": "xfce4-terminal -x",
|
||||
"gnome-terminal": "gnome-terminal --window --",
|
||||
}
|
||||
EDITORS: list[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
|
||||
|
||||
class IndentDumper(yaml.Dumper):
|
||||
|
|
@ -46,17 +46,17 @@ class IndentDumper(yaml.Dumper):
|
|||
|
||||
class CustomNode(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CustomNode"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, image: str, services: list[str]) -> None:
|
||||
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
||||
self.name: str = name
|
||||
self.image: str = image
|
||||
self.services: list[str] = services
|
||||
self.services: List[str] = services
|
||||
|
||||
|
||||
class CoreServer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CoreServer"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, address: str) -> None:
|
||||
self.name: str = name
|
||||
|
|
@ -65,7 +65,7 @@ class CoreServer(yaml.YAMLObject):
|
|||
|
||||
class Observer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!Observer"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, cmd: str) -> None:
|
||||
self.name: str = name
|
||||
|
|
@ -74,7 +74,7 @@ class Observer(yaml.YAMLObject):
|
|||
|
||||
class PreferencesConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!PreferencesConfig"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -95,7 +95,7 @@ class PreferencesConfig(yaml.YAMLObject):
|
|||
|
||||
class LocationConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!LocationConfig"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -118,34 +118,41 @@ class LocationConfig(yaml.YAMLObject):
|
|||
|
||||
class IpConfigs(yaml.YAMLObject):
|
||||
yaml_tag: str = "!IpConfigs"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__setstate__(kwargs)
|
||||
|
||||
def __setstate__(self, kwargs):
|
||||
self.ip4s: list[str] = kwargs.get(
|
||||
"ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
)
|
||||
self.ip4: str = kwargs.get("ip4", self.ip4s[0])
|
||||
self.ip6s: list[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"])
|
||||
self.ip6: str = kwargs.get("ip6", self.ip6s[0])
|
||||
self.enable_ip4: bool = kwargs.get("enable_ip4", True)
|
||||
self.enable_ip6: bool = kwargs.get("enable_ip6", True)
|
||||
def __init__(
|
||||
self,
|
||||
ip4: str = None,
|
||||
ip6: str = None,
|
||||
ip4s: List[str] = None,
|
||||
ip6s: List[str] = None,
|
||||
) -> None:
|
||||
if ip4s is None:
|
||||
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
self.ip4s: List[str] = ip4s
|
||||
if ip6s is None:
|
||||
ip6s = ["2001::", "2002::", "a::"]
|
||||
self.ip6s: List[str] = ip6s
|
||||
if ip4 is None:
|
||||
ip4 = self.ip4s[0]
|
||||
self.ip4: str = ip4
|
||||
if ip6 is None:
|
||||
ip6 = self.ip6s[0]
|
||||
self.ip6: str = ip6
|
||||
|
||||
|
||||
class GuiConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!GuiConfig"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preferences: PreferencesConfig = None,
|
||||
location: LocationConfig = None,
|
||||
servers: list[CoreServer] = None,
|
||||
nodes: list[CustomNode] = None,
|
||||
recentfiles: list[str] = None,
|
||||
observers: list[Observer] = None,
|
||||
servers: List[CoreServer] = None,
|
||||
nodes: List[CustomNode] = None,
|
||||
recentfiles: List[str] = None,
|
||||
observers: List[Observer] = None,
|
||||
scale: float = 1.0,
|
||||
ips: IpConfigs = None,
|
||||
mac: str = "00:00:00:aa:00:00",
|
||||
|
|
@ -158,16 +165,16 @@ class GuiConfig(yaml.YAMLObject):
|
|||
self.location: LocationConfig = location
|
||||
if servers is None:
|
||||
servers = []
|
||||
self.servers: list[CoreServer] = servers
|
||||
self.servers: List[CoreServer] = servers
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
self.nodes: list[CustomNode] = nodes
|
||||
self.nodes: List[CustomNode] = nodes
|
||||
if recentfiles is None:
|
||||
recentfiles = []
|
||||
self.recentfiles: list[str] = recentfiles
|
||||
self.recentfiles: List[str] = recentfiles
|
||||
if observers is None:
|
||||
observers = []
|
||||
self.observers: list[Observer] = observers
|
||||
self.observers: List[Observer] = observers
|
||||
self.scale: float = scale
|
||||
if ips is None:
|
||||
ips = IpConfigs()
|
||||
|
|
@ -178,8 +185,7 @@ class GuiConfig(yaml.YAMLObject):
|
|||
def copy_files(current_path: Path, new_path: Path) -> None:
|
||||
for current_file in current_path.glob("*"):
|
||||
new_file = new_path.joinpath(current_file.name)
|
||||
if not new_file.exists():
|
||||
shutil.copy(current_file, new_file)
|
||||
shutil.copy(current_file, new_file)
|
||||
|
||||
|
||||
def find_terminal() -> Optional[str]:
|
||||
|
|
@ -191,32 +197,35 @@ def find_terminal() -> Optional[str]:
|
|||
|
||||
|
||||
def check_directory() -> None:
|
||||
HOME_PATH.mkdir(exist_ok=True)
|
||||
BACKGROUNDS_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
|
||||
ICONS_PATH.mkdir(exist_ok=True)
|
||||
MOBILITY_PATH.mkdir(exist_ok=True)
|
||||
XMLS_PATH.mkdir(exist_ok=True)
|
||||
SCRIPT_PATH.mkdir(exist_ok=True)
|
||||
if HOME_PATH.exists():
|
||||
return
|
||||
HOME_PATH.mkdir()
|
||||
BACKGROUNDS_PATH.mkdir()
|
||||
CUSTOM_EMANE_PATH.mkdir()
|
||||
CUSTOM_SERVICE_PATH.mkdir()
|
||||
ICONS_PATH.mkdir()
|
||||
MOBILITY_PATH.mkdir()
|
||||
XMLS_PATH.mkdir()
|
||||
SCRIPT_PATH.mkdir()
|
||||
|
||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||
if not CONFIG_PATH.exists():
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
|
||||
|
||||
def read() -> GuiConfig:
|
||||
with CONFIG_PATH.open("r") as f:
|
||||
return yaml.safe_load(f)
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
|
||||
|
||||
def save(config: GuiConfig) -> None:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
BIN
daemon/core/gui/data/icons/antenna.gif
Normal file
BIN
daemon/core/gui/data/icons/antenna.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 B |
Binary file not shown.
|
Before Width: | Height: | Size: 385 B |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 326 B |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -1,476 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/tmp/tmpd4t2sxy2">
|
||||
<networks>
|
||||
<network id="5" name="wlan5" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="388.0" y="555.0" lat="47.574121408201655" lon="-122.12709602379641" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="258.0" y="147.0" lat="47.57783021533393" lon="-122.12884773860046" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="526.0" y="147.0" lat="47.57783021533393" lon="-122.12523651115826" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="3" name="n3" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="241.0" y="387.0" lat="47.575648595893774" lon="-122.1290768089979" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="4" name="n4" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="529.0" y="385.0" lat="47.57566677643136" lon="-122.12519608697049" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="1" node2="5">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="2" node2="5">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="3" node2="5">
|
||||
<iface1 nem="3" id="0" name="eth0" mac="02:02:00:00:00:03" ip4="10.0.0.3" ip4_mask="32" ip6="2001::3" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="4" node2="5">
|
||||
<iface1 nem="4" id="0" name="eth0" mac="02:02:00:00:00:04" ip4="10.0.0.4" ip4_mask="32" ip6="2001::4" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="1" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="0"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="2" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="5.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="4" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="0"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="5" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
<configuration name="controlnet1" value=""/>
|
||||
<configuration name="controlnet2" value=""/>
|
||||
<configuration name="controlnet3" value=""/>
|
||||
<configuration name="controlnet_updown_script" value=""/>
|
||||
<configuration name="enablerj45" value="1"/>
|
||||
<configuration name="preservedir" value="0"/>
|
||||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="PC">
|
||||
<service name="DefaultRoute"/>
|
||||
</node>
|
||||
<node type="prouter"/>
|
||||
<node type="router">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv2"/>
|
||||
<service name="OSPFv3"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="host">
|
||||
<service name="DefaultRoute"/>
|
||||
<service name="SSH"/>
|
||||
</node>
|
||||
</default_services>
|
||||
</scenario>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/tmp/tmp2mkcwn17">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="172.0" lat="47.577602967549986" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
<configuration name="controlnet1" value=""/>
|
||||
<configuration name="controlnet2" value=""/>
|
||||
<configuration name="controlnet3" value=""/>
|
||||
<configuration name="controlnet_updown_script" value=""/>
|
||||
<configuration name="enablerj45" value="1"/>
|
||||
<configuration name="preservedir" value="0"/>
|
||||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="PC">
|
||||
<service name="DefaultRoute"/>
|
||||
</node>
|
||||
<node type="prouter"/>
|
||||
<node type="router">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv2"/>
|
||||
<service name="OSPFv3"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="host">
|
||||
<service name="DefaultRoute"/>
|
||||
<service name="SSH"/>
|
||||
</node>
|
||||
</default_services>
|
||||
</scenario>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/tmp/tmpsj4dhmce">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="173.0" lat="47.57759387761812" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
<configuration name="controlnet1" value=""/>
|
||||
<configuration name="controlnet2" value=""/>
|
||||
<configuration name="controlnet3" value=""/>
|
||||
<configuration name="controlnet_updown_script" value=""/>
|
||||
<configuration name="enablerj45" value="1"/>
|
||||
<configuration name="preservedir" value="0"/>
|
||||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="PC">
|
||||
<service name="DefaultRoute"/>
|
||||
</node>
|
||||
<node type="prouter"/>
|
||||
<node type="router">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv2"/>
|
||||
<service name="OSPFv3"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="host">
|
||||
<service name="DefaultRoute"/>
|
||||
<service name="SSH"/>
|
||||
</node>
|
||||
</default_services>
|
||||
</scenario>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/tmp/tmp081pn3j9">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="173.0" lat="47.57759387761812" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
<configuration name="controlnet1" value=""/>
|
||||
<configuration name="controlnet2" value=""/>
|
||||
<configuration name="controlnet3" value=""/>
|
||||
<configuration name="controlnet_updown_script" value=""/>
|
||||
<configuration name="enablerj45" value="1"/>
|
||||
<configuration name="preservedir" value="0"/>
|
||||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="PC">
|
||||
<service name="DefaultRoute"/>
|
||||
</node>
|
||||
<node type="prouter"/>
|
||||
<node type="router">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv2"/>
|
||||
<service name="OSPFv3"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="host">
|
||||
<service name="DefaultRoute"/>
|
||||
<service name="SSH"/>
|
||||
</node>
|
||||
</default_services>
|
||||
</scenario>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/tmp/tmpfokvqh5k">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="172.0" lat="47.577602967549986" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
<configuration name="controlnet1" value=""/>
|
||||
<configuration name="controlnet2" value=""/>
|
||||
<configuration name="controlnet3" value=""/>
|
||||
<configuration name="controlnet_updown_script" value=""/>
|
||||
<configuration name="enablerj45" value="1"/>
|
||||
<configuration name="preservedir" value="0"/>
|
||||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="PC">
|
||||
<service name="DefaultRoute"/>
|
||||
</node>
|
||||
<node type="prouter"/>
|
||||
<node type="router">
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv2"/>
|
||||
<service name="OSPFv3"/>
|
||||
<service name="IPForward"/>
|
||||
</node>
|
||||
<node type="host">
|
||||
<service name="DefaultRoute"/>
|
||||
<service name="SSH"/>
|
||||
</node>
|
||||
</default_services>
|
||||
</scenario>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -46,9 +46,9 @@ class AboutDialog(Dialog):
|
|||
codetext = CodeText(self.top)
|
||||
codetext.text.insert("1.0", LICENSE)
|
||||
codetext.text.config(state=tk.DISABLED)
|
||||
codetext.grid(sticky=tk.NSEW)
|
||||
codetext.grid(sticky="nsew")
|
||||
|
||||
label = ttk.Label(
|
||||
self.top, text="Icons from https://icons8.com", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(sticky=tk.EW)
|
||||
label.grid(sticky="ew")
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ check engine light
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel
|
||||
from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
|
@ -19,7 +19,7 @@ class AlertsDialog(Dialog):
|
|||
super().__init__(app, "Alerts")
|
||||
self.tree: Optional[ttk.Treeview] = None
|
||||
self.codetext: Optional[CodeText] = None
|
||||
self.alarm_map: dict[int, ExceptionEvent] = {}
|
||||
self.alarm_map: Dict[int, ExceptionEvent] = {}
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
@ -30,13 +30,13 @@ class AlertsDialog(Dialog):
|
|||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame,
|
||||
columns=("time", "level", "session_id", "node", "source"),
|
||||
show="headings",
|
||||
)
|
||||
self.tree.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
self.tree.column("time", stretch=tk.YES)
|
||||
self.tree.heading("time", text="Time")
|
||||
self.tree.column("level", stretch=tk.YES, width=100)
|
||||
|
|
@ -49,9 +49,9 @@ class AlertsDialog(Dialog):
|
|||
self.tree.heading("source", text="Source")
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
for exception in self.app.statusbar.core_alarms:
|
||||
level_name = exception.level.name
|
||||
node_id = exception.node_id if exception.node_id else ""
|
||||
for alarm in self.app.statusbar.core_alarms:
|
||||
exception = alarm.exception_event
|
||||
level_name = ExceptionLevel.Enum.Name(exception.level)
|
||||
insert_id = self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
|
|
@ -59,56 +59,54 @@ class AlertsDialog(Dialog):
|
|||
values=(
|
||||
exception.date,
|
||||
level_name,
|
||||
exception.session_id,
|
||||
node_id,
|
||||
alarm.session_id,
|
||||
exception.node_id,
|
||||
exception.source,
|
||||
),
|
||||
tags=(level_name,),
|
||||
)
|
||||
self.alarm_map[insert_id] = exception
|
||||
self.alarm_map[insert_id] = alarm
|
||||
|
||||
error_name = ExceptionLevel.ERROR.name
|
||||
error_name = ExceptionLevel.Enum.Name(ExceptionLevel.ERROR)
|
||||
self.tree.tag_configure(error_name, background="#ff6666")
|
||||
fatal_name = ExceptionLevel.FATAL.name
|
||||
fatal_name = ExceptionLevel.Enum.Name(ExceptionLevel.FATAL)
|
||||
self.tree.tag_configure(fatal_name, background="#d9d9d9")
|
||||
warning_name = ExceptionLevel.WARNING.name
|
||||
warning_name = ExceptionLevel.Enum.Name(ExceptionLevel.WARNING)
|
||||
self.tree.tag_configure(warning_name, background="#ffff99")
|
||||
notice_name = ExceptionLevel.NOTICE.name
|
||||
notice_name = ExceptionLevel.Enum.Name(ExceptionLevel.NOTICE)
|
||||
self.tree.tag_configure(notice_name, background="#85e085")
|
||||
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky=tk.EW)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
self.codetext = CodeText(self.top)
|
||||
self.codetext.text.config(state=tk.DISABLED, height=11)
|
||||
self.codetext.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.codetext.grid(sticky="nsew", pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Close", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def reset_alerts(self) -> None:
|
||||
self.codetext.text.config(state=tk.NORMAL)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
self.codetext.text.config(state=tk.DISABLED)
|
||||
self.codetext.text.delete("1.0", tk.END)
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
self.app.statusbar.clear_alerts()
|
||||
self.app.statusbar.core_alarms.clear()
|
||||
|
||||
def click_select(self, event: tk.Event) -> None:
|
||||
current = self.tree.selection()[0]
|
||||
exception = self.alarm_map[current]
|
||||
alarm = self.alarm_map[current]
|
||||
self.codetext.text.config(state=tk.NORMAL)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
self.codetext.text.insert(1.0, exception.text)
|
||||
self.codetext.text.delete("1.0", "end")
|
||||
self.codetext.text.insert("1.0", alarm.exception_event.text)
|
||||
self.codetext.text.config(state=tk.DISABLED)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -22,12 +22,12 @@ class SizeAndScaleDialog(Dialog):
|
|||
create an instance for size and scale object
|
||||
"""
|
||||
super().__init__(app, "Canvas Size and Scale")
|
||||
self.manager: CanvasManager = self.app.manager
|
||||
self.section_font: font.Font = font.Font(weight=font.BOLD)
|
||||
width, height = self.manager.current().current_dimensions
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.section_font: font.Font = font.Font(weight="bold")
|
||||
width, height = self.canvas.current_dimensions
|
||||
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
|
||||
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
|
||||
location = self.app.core.session.location
|
||||
location = self.app.core.location
|
||||
self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
|
||||
self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
|
||||
self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
|
||||
|
|
@ -54,68 +54,68 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def draw_size(self) -> None:
|
||||
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# draw size row 1
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="Pixels")
|
||||
label.grid(row=0, column=4, sticky=tk.W)
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
|
||||
# draw size row 2
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, textvariable=self.meters_width, state=tk.DISABLED
|
||||
)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, textvariable=self.meters_height, state=tk.DISABLED
|
||||
)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=4, sticky=tk.W)
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
|
||||
def draw_scale(self) -> None:
|
||||
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=2, sticky=tk.W)
|
||||
label.grid(row=0, column=2, sticky="w")
|
||||
|
||||
def draw_reference_point(self) -> None:
|
||||
label_frame = ttk.Labelframe(
|
||||
self.top, text="Reference Point", padding=FRAME_PAD
|
||||
)
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
label = ttk.Label(
|
||||
|
|
@ -124,61 +124,61 @@ class SizeAndScaleDialog(Dialog):
|
|||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="X")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.x)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Y")
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.y)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(label_frame, text="Translates To")
|
||||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.columnconfigure(5, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Lat")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.lat)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Lon")
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.lon)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Alt")
|
||||
label.grid(row=0, column=4, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=4, sticky="w", padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.alt)
|
||||
entry.grid(row=0, column=5, sticky=tk.EW)
|
||||
entry.grid(row=0, column=5, sticky="ew")
|
||||
|
||||
def draw_save_as_default(self) -> None:
|
||||
button = ttk.Checkbutton(
|
||||
self.top, text="Save as default?", variable=self.save_default
|
||||
)
|
||||
button.grid(sticky=tk.W, pady=PADY)
|
||||
button.grid(sticky="w", pady=PADY)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def size_scale_keyup(self, _event: tk.Event) -> None:
|
||||
scale = self.scale.get()
|
||||
|
|
@ -189,8 +189,10 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def click_apply(self) -> None:
|
||||
width, height = self.pixel_width.get(), self.pixel_height.get()
|
||||
self.manager.redraw_canvas((width, height))
|
||||
location = self.app.core.session.location
|
||||
self.canvas.redraw_canvas((width, height))
|
||||
if self.canvas.wallpaper:
|
||||
self.canvas.redraw_wallpaper()
|
||||
location = self.app.core.location
|
||||
location.x = self.x.get()
|
||||
location.y = self.y.get()
|
||||
location.lat = self.lat.get()
|
||||
|
|
|
|||
|
|
@ -4,17 +4,15 @@ set wallpaper
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
|
@ -25,14 +23,14 @@ class CanvasWallpaperDialog(Dialog):
|
|||
create an instance of CanvasWallpaper object
|
||||
"""
|
||||
super().__init__(app, "Canvas Background")
|
||||
self.canvas: CanvasGraph = self.app.manager.current()
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
|
||||
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
|
||||
value=self.canvas.adjust_to_dim.get()
|
||||
)
|
||||
self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
|
||||
self.image_label: Optional[ttk.Label] = None
|
||||
self.options: list[ttk.Radiobutton] = []
|
||||
self.options: List[ttk.Radiobutton] = []
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
@ -53,7 +51,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
|
||||
def draw_image_label(self) -> None:
|
||||
label = ttk.Label(self.top, text="Image filename: ")
|
||||
label.grid(sticky=tk.EW)
|
||||
label.grid(sticky="ew")
|
||||
if self.filename.get():
|
||||
self.draw_preview()
|
||||
|
||||
|
|
@ -62,17 +60,17 @@ class CanvasWallpaperDialog(Dialog):
|
|||
frame.columnconfigure(0, weight=2)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
entry = ttk.Entry(frame, textvariable=self.filename)
|
||||
entry.focus()
|
||||
entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="...", command=self.click_open_image)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Clear", command=self.click_clear)
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_options(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
|
|
@ -80,30 +78,30 @@ class CanvasWallpaperDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew")
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="upper-left", value=1, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=0, sticky=tk.EW)
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="centered", value=2, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="scaled", value=3, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="titled", value=4, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
self.options.append(button)
|
||||
|
||||
def draw_additional_options(self) -> None:
|
||||
|
|
@ -113,19 +111,19 @@ class CanvasWallpaperDialog(Dialog):
|
|||
variable=self.adjust_to_dim,
|
||||
command=self.click_adjust_canvas,
|
||||
)
|
||||
checkbutton.grid(sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
checkbutton.grid(sticky="ew", padx=PADX)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_open_image(self) -> None:
|
||||
filename = image_chooser(self, BACKGROUNDS_PATH)
|
||||
|
|
@ -134,7 +132,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
self.draw_preview()
|
||||
|
||||
def draw_preview(self) -> None:
|
||||
image = images.from_file(self.filename.get(), width=250, height=135)
|
||||
image = Images.create(self.filename.get(), 250, 135)
|
||||
self.image_label.config(image=image)
|
||||
self.image_label.image = image
|
||||
|
||||
|
|
@ -163,11 +161,12 @@ class CanvasWallpaperDialog(Dialog):
|
|||
def click_apply(self) -> None:
|
||||
self.canvas.scale_option.set(self.scale_option.get())
|
||||
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
||||
self.canvas.show_grid.click_handler()
|
||||
filename = self.filename.get()
|
||||
if not filename:
|
||||
filename = None
|
||||
try:
|
||||
self.canvas.set_wallpaper(filename)
|
||||
except FileNotFoundError:
|
||||
logger.error("invalid background: %s", filename)
|
||||
logging.error("invalid background: %s", filename)
|
||||
self.destroy()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ custom color picker
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
|
@ -13,36 +13,6 @@ if TYPE_CHECKING:
|
|||
from core.gui.app import Application
|
||||
|
||||
|
||||
def get_rgb(red: int, green: int, blue: int) -> str:
|
||||
"""
|
||||
Convert rgb integers to an rgb hex code (#<red><green><blue>).
|
||||
|
||||
:param red: red value
|
||||
:param green: green value
|
||||
:param blue: blue value
|
||||
:return: rgb hex code
|
||||
"""
|
||||
return f"#{red:02x}{green:02x}{blue:02x}"
|
||||
|
||||
|
||||
def get_rgb_values(hex_code: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
Convert a valid rgb hex code (#<red><green><blue>) to rgb integers.
|
||||
|
||||
:param hex_code: valid rgb hex code
|
||||
:return: a tuple of red, blue, and green values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
green = hex_code[2]
|
||||
blue = hex_code[3]
|
||||
else:
|
||||
red = hex_code[1:3]
|
||||
green = hex_code[3:5]
|
||||
blue = hex_code[5:]
|
||||
return int(red, 16), int(green, 16), int(blue, 16)
|
||||
|
||||
|
||||
class ColorPickerDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
|
||||
|
|
@ -57,7 +27,7 @@ class ColorPickerDialog(Dialog):
|
|||
self.blue_label: Optional[ttk.Label] = None
|
||||
self.display: Optional[tk.Frame] = None
|
||||
self.color: str = initcolor
|
||||
red, green, blue = get_rgb_values(initcolor)
|
||||
red, green, blue = self.get_rgb(initcolor)
|
||||
self.red: tk.IntVar = tk.IntVar(value=red)
|
||||
self.blue: tk.IntVar = tk.IntVar(value=blue)
|
||||
self.green: tk.IntVar = tk.IntVar(value=green)
|
||||
|
|
@ -78,13 +48,13 @@ class ColorPickerDialog(Dialog):
|
|||
|
||||
# rgb frames
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=0, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="R")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.red_entry = validation.RgbEntry(frame, width=3, textvariable=self.red)
|
||||
self.red_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
self.red_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
|
@ -94,20 +64,20 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.red_scale,
|
||||
command=lambda x: self.scale_callback(self.red_scale, self.red),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
self.red_label = ttk.Label(
|
||||
frame, background=get_rgb(self.red.get(), 0, 0), width=5
|
||||
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
|
||||
)
|
||||
self.red_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
self.red_label.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="G")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.green_entry = validation.RgbEntry(frame, width=3, textvariable=self.green)
|
||||
self.green_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
self.green_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
|
@ -117,20 +87,20 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.green_scale,
|
||||
command=lambda x: self.scale_callback(self.green_scale, self.green),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
self.green_label = ttk.Label(
|
||||
frame, background=get_rgb(0, self.green.get(), 0), width=5
|
||||
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
|
||||
)
|
||||
self.green_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
self.green_label.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="B")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.blue_entry = validation.RgbEntry(frame, width=3, textvariable=self.blue)
|
||||
self.blue_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
self.blue_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
|
@ -140,31 +110,31 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.blue_scale,
|
||||
command=lambda x: self.scale_callback(self.blue_scale, self.blue),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
self.blue_label = ttk.Label(
|
||||
frame, background=get_rgb(0, 0, self.blue.get()), width=5
|
||||
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
|
||||
)
|
||||
self.blue_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
self.blue_label.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
# hex code and color display
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(1, weight=1)
|
||||
self.hex_entry = validation.HexEntry(frame, textvariable=self.hex)
|
||||
self.hex_entry.grid(sticky=tk.EW, pady=PADY)
|
||||
self.hex_entry.grid(sticky="ew", pady=PADY)
|
||||
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
|
||||
self.display.grid(sticky=tk.NSEW)
|
||||
frame.grid(row=3, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
self.display.grid(sticky="nsew")
|
||||
frame.grid(row=3, column=0, sticky="nsew", pady=PADY)
|
||||
|
||||
# button frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=4, column=0, sticky=tk.EW)
|
||||
frame.grid(row=4, column=0, sticky="ew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="OK", command=self.button_ok)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def set_bindings(self) -> None:
|
||||
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||
|
|
@ -180,27 +150,39 @@ class ColorPickerDialog(Dialog):
|
|||
self.color = self.hex.get()
|
||||
self.destroy()
|
||||
|
||||
def get_hex(self) -> str:
|
||||
"""
|
||||
convert current RGB values into hex color
|
||||
"""
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
|
||||
def current_focus(self, focus: str) -> None:
|
||||
self.focus = focus
|
||||
|
||||
def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
|
||||
if self.focus == "rgb":
|
||||
red = int(self.red_entry.get() or 0)
|
||||
blue = int(self.blue_entry.get() or 0)
|
||||
green = int(self.green_entry.get() or 0)
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
self.set_scale(red, green, blue)
|
||||
hex_code = get_rgb(red, green, blue)
|
||||
self.hex.set(hex_code)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
if red and blue and green:
|
||||
hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
self.hex.set(hex_code)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
elif self.focus == "hex":
|
||||
hex_code = self.hex.get()
|
||||
if len(hex_code) == 4 or len(hex_code) == 7:
|
||||
red, green, blue = get_rgb_values(hex_code)
|
||||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
red, green, blue = self.get_rgb(hex_code)
|
||||
else:
|
||||
return
|
||||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(str(red), str(green), str(blue))
|
||||
|
||||
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
|
||||
color_var.set(var.get())
|
||||
|
|
@ -217,7 +199,21 @@ class ColorPickerDialog(Dialog):
|
|||
self.green.set(green)
|
||||
self.blue.set(blue)
|
||||
|
||||
def set_label(self, red: int, green: int, blue: int) -> None:
|
||||
self.red_label.configure(background=get_rgb(red, 0, 0))
|
||||
self.green_label.configure(background=get_rgb(0, green, 0))
|
||||
self.blue_label.configure(background=get_rgb(0, 0, blue))
|
||||
def set_label(self, red: str, green: str, blue: str) -> None:
|
||||
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
||||
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
||||
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
||||
|
||||
def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
|
||||
"""
|
||||
convert a valid hex code to RGB values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
green = hex_code[2]
|
||||
blue = hex_code[3]
|
||||
else:
|
||||
red = hex_code[1:3]
|
||||
green = hex_code[3:5]
|
||||
blue = hex_code[5:]
|
||||
return int(red, 16), int(green, 16), int(blue, 16)
|
||||
|
|
|
|||
|
|
@ -4,53 +4,55 @@ Service configuration dialog
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.wrappers import (
|
||||
ConfigOption,
|
||||
ConfigServiceData,
|
||||
Node,
|
||||
ServiceValidationMode,
|
||||
)
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.services_pb2 import ServiceValidationMode
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
||||
|
||||
class ConfigServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
|
||||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
service_name: str,
|
||||
canvas_node: "CanvasNode",
|
||||
node_id: int,
|
||||
) -> None:
|
||||
title = f"{service_name} Config Service"
|
||||
super().__init__(app, title, master=master)
|
||||
self.core: "CoreClient" = app.core
|
||||
self.node: Node = node
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node_id: int = node_id
|
||||
self.service_name: str = service_name
|
||||
self.radiovar: tk.IntVar = tk.IntVar(value=2)
|
||||
self.directories: list[str] = []
|
||||
self.templates: list[str] = []
|
||||
self.rendered: dict[str, str] = {}
|
||||
self.dependencies: list[str] = []
|
||||
self.executables: list[str] = []
|
||||
self.startup_commands: list[str] = []
|
||||
self.validation_commands: list[str] = []
|
||||
self.shutdown_commands: list[str] = []
|
||||
self.default_startup: list[str] = []
|
||||
self.default_validate: list[str] = []
|
||||
self.default_shutdown: list[str] = []
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.directories: List[str] = []
|
||||
self.templates: List[str] = []
|
||||
self.dependencies: List[str] = []
|
||||
self.executables: List[str] = []
|
||||
self.startup_commands: List[str] = []
|
||||
self.validation_commands: List[str] = []
|
||||
self.shutdown_commands: List[str] = []
|
||||
self.default_startup: List[str] = []
|
||||
self.default_validate: List[str] = []
|
||||
self.default_shutdown: List[str] = []
|
||||
self.validation_mode: Optional[ServiceValidationMode] = None
|
||||
self.validation_time: Optional[int] = None
|
||||
self.validation_period: tk.DoubleVar = tk.DoubleVar()
|
||||
self.modes: list[str] = []
|
||||
self.mode_configs: dict[str, dict[str, str]] = {}
|
||||
self.validation_period: tk.StringVar = tk.StringVar()
|
||||
self.modes: List[str] = []
|
||||
self.mode_configs: Dict[str, str] = {}
|
||||
|
||||
self.notebook: Optional[ttk.Notebook] = None
|
||||
self.templates_combobox: Optional[ttk.Combobox] = None
|
||||
self.modes_combobox: Optional[ttk.Combobox] = None
|
||||
|
|
@ -60,14 +62,13 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.validation_time_entry: Optional[ttk.Entry] = None
|
||||
self.validation_mode_entry: Optional[ttk.Entry] = None
|
||||
self.template_text: Optional[CodeText] = None
|
||||
self.rendered_text: Optional[CodeText] = None
|
||||
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||
self.original_service_files: dict[str, str] = {}
|
||||
self.temp_service_files: dict[str, str] = {}
|
||||
self.modified_files: set[str] = set()
|
||||
self.original_service_files: Dict[str, str] = {}
|
||||
self.temp_service_files: Dict[str, str] = {}
|
||||
self.modified_files: Set[str] = set()
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.default_config: dict[str, str] = {}
|
||||
self.config: dict[str, ConfigOption] = {}
|
||||
self.default_config: Dict[str, str] = {}
|
||||
self.config: Dict[str, ConfigOption] = {}
|
||||
self.has_error: bool = False
|
||||
self.load()
|
||||
if not self.has_error:
|
||||
|
|
@ -75,7 +76,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def load(self) -> None:
|
||||
try:
|
||||
self.core.start_session(definition=True)
|
||||
self.core.create_nodes_and_links()
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
|
|
@ -87,26 +88,29 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.validation_mode = service.validation_mode
|
||||
self.validation_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
defaults = self.core.get_config_service_defaults(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
self.original_service_files = defaults.templates
|
||||
|
||||
response = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = response.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
self.modes = sorted(defaults.modes)
|
||||
self.mode_configs = defaults.modes
|
||||
self.config = ConfigOption.from_dict(defaults.config)
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
self.rendered = self.core.get_config_service_rendered(
|
||||
self.node.id, self.service_name
|
||||
|
||||
self.modes = sorted(x.name for x in response.modes)
|
||||
self.mode_configs = {x.name: x.config for x in response.modes}
|
||||
|
||||
service_config = self.canvas_node.config_service_configs.get(
|
||||
self.service_name, {}
|
||||
)
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if service_config:
|
||||
for key, value in service_config.config.items():
|
||||
self.config = response.config
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
custom_config = service_config.get("config")
|
||||
if custom_config:
|
||||
for key, value in custom_config.items():
|
||||
self.config[key].value = value
|
||||
logger.info("default config: %s", self.default_config)
|
||||
for file, data in service_config.templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
logging.info("default config: %s", self.default_config)
|
||||
|
||||
custom_templates = service_config.get("templates", {})
|
||||
for file, data in custom_templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Config Service Error", e)
|
||||
self.has_error = True
|
||||
|
|
@ -114,9 +118,10 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_tab_files()
|
||||
if self.config:
|
||||
self.draw_tab_config()
|
||||
|
|
@ -126,9 +131,8 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_files(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
tab.rowconfigure(2, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
|
|
@ -137,68 +141,47 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
label.grid(pady=PADY)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
state = "readonly" if self.directories else tk.DISABLED
|
||||
directories_combobox = ttk.Combobox(frame, values=self.directories, state=state)
|
||||
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
label = ttk.Label(frame, text="Files")
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
state = "readonly" if self.templates else tk.DISABLED
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state=state
|
||||
frame, values=self.templates, state="readonly"
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
# draw file template tab
|
||||
notebook = ttk.Notebook(tab)
|
||||
notebook.rowconfigure(0, weight=1)
|
||||
notebook.columnconfigure(0, weight=1)
|
||||
notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
# draw rendered file tab
|
||||
rendered_tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
rendered_tab.grid(sticky=tk.NSEW)
|
||||
rendered_tab.rowconfigure(0, weight=1)
|
||||
rendered_tab.columnconfigure(0, weight=1)
|
||||
notebook.add(rendered_tab, text="Rendered")
|
||||
self.rendered_text = CodeText(rendered_tab)
|
||||
self.rendered_text.grid(sticky=tk.NSEW)
|
||||
self.rendered_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
# draw template file tab
|
||||
template_tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
template_tab.grid(sticky=tk.NSEW)
|
||||
template_tab.rowconfigure(0, weight=1)
|
||||
template_tab.columnconfigure(0, weight=1)
|
||||
notebook.add(template_tab, text="Template")
|
||||
self.template_text = CodeText(template_tab)
|
||||
self.template_text.grid(sticky=tk.NSEW)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
self.template_text.grid(sticky="nsew")
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
template_name = self.templates[0]
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered_data)
|
||||
else:
|
||||
self.template_text.text.configure(state=tk.DISABLED)
|
||||
self.rendered_text.text.configure(state=tk.DISABLED)
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert(
|
||||
"end", self.temp_service_files[self.templates[0]]
|
||||
)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
|
||||
def draw_tab_config(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration")
|
||||
|
||||
if self.modes:
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Modes")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
|
|
@ -206,17 +189,17 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
frame, values=self.modes, state="readonly"
|
||||
)
|
||||
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
logger.info("config service config: %s", self.config)
|
||||
logging.info("config service config: %s", self.config)
|
||||
self.config_frame = ConfigFrame(tab, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||
|
||||
def draw_tab_startstop(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
|
|
@ -242,12 +225,12 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
|
|
@ -257,23 +240,23 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_validation(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.EW)
|
||||
tab.grid(sticky="ew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Validation", sticky=tk.NSEW)
|
||||
self.notebook.add(tab, text="Validation", sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", str(self.validation_time))
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
if self.validation_mode == ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == ServiceValidationMode.NON_BLOCKING:
|
||||
|
|
@ -285,88 +268,85 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky=tk.W, padx=PADX)
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=self.validation_period
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_apply(self) -> None:
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom():
|
||||
self.node.config_service_configs.pop(self.service_name, None)
|
||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
service_config = self.node.config_service_configs.setdefault(
|
||||
self.service_name, ConfigServiceData()
|
||||
|
||||
service_config = self.canvas_node.config_service_configs.setdefault(
|
||||
self.service_name, {}
|
||||
)
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
service_config.config = {x.name: x.value for x in self.config.values()}
|
||||
service_config["config"] = {x.name: x.value for x in self.config.values()}
|
||||
templates_config = service_config.setdefault("templates", {})
|
||||
for file in self.modified_files:
|
||||
service_config.templates[file] = self.temp_service_files[file]
|
||||
templates_config[file] = self.temp_service_files[file]
|
||||
all_current = current_listbox.get(0, tk.END)
|
||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event) -> None:
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered)
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
|
||||
def handle_mode_changed(self, event: tk.Event) -> None:
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logger.info("mode config: %s", config)
|
||||
logging.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, _event: tk.Event) -> None:
|
||||
def update_template_file_data(self, event: tk.Event) -> None:
|
||||
scrolledtext = event.widget
|
||||
template = self.templates_combobox.get()
|
||||
self.temp_service_files[template] = self.rendered_text.get_text()
|
||||
if self.rendered[template] != self.temp_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
return
|
||||
self.temp_service_files[template] = self.template_text.get_text()
|
||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[template] != self.original_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
|
|
@ -381,33 +361,23 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self) -> None:
|
||||
# clear all saved state data
|
||||
self.modified_files.clear()
|
||||
self.node.config_service_configs.pop(self.service_name, None)
|
||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||
logging.info(
|
||||
"cleared config service config: %s", self.canvas_node.config_service_configs
|
||||
)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
# reset session definition and retrieve default rendered templates
|
||||
self.core.start_session(definition=True)
|
||||
self.rendered = self.core.get_config_service_rendered(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
logger.info(
|
||||
"cleared config service config: %s", self.node.config_service_configs
|
||||
)
|
||||
# reset current selected file data and config data, if present
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered_data)
|
||||
filename = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[filename])
|
||||
if self.config_frame:
|
||||
logger.info("resetting defaults: %s", self.default_config)
|
||||
logging.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
||||
def click_copy(self) -> None:
|
||||
pass
|
||||
|
||||
def append_commands(
|
||||
self, commands: list[str], listbox: tk.Listbox, to_add: list[str]
|
||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||
) -> None:
|
||||
for cmd in to_add:
|
||||
commands.append(cmd)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ copy service config dialog
|
|||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
|
@ -29,7 +29,7 @@ class CopyServiceConfigDialog(Dialog):
|
|||
self.service: str = service
|
||||
self.file_name: str = file_name
|
||||
self.listbox: Optional[tk.Listbox] = None
|
||||
self.nodes: dict[str, int] = {}
|
||||
self.nodes: Dict[str, int] = {}
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
@ -38,40 +38,41 @@ class CopyServiceConfigDialog(Dialog):
|
|||
label = ttk.Label(
|
||||
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
self.listbox = listbox_scroll.listbox
|
||||
for node in self.app.core.session.nodes.values():
|
||||
file_configs = node.service_file_configs.get(self.service)
|
||||
for canvas_node in self.app.canvas.nodes.values():
|
||||
file_configs = canvas_node.service_file_configs.get(self.service)
|
||||
if not file_configs:
|
||||
continue
|
||||
data = file_configs.get(self.file_name)
|
||||
if not data:
|
||||
continue
|
||||
self.nodes[node.name] = node.id
|
||||
self.listbox.insert(tk.END, node.name)
|
||||
name = canvas_node.core_node.name
|
||||
self.nodes[name] = canvas_node.id
|
||||
self.listbox.insert(tk.END, name)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Copy", command=self.click_copy)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="View", command=self.click_view)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def click_copy(self) -> None:
|
||||
selection = self.listbox.curselection()
|
||||
if not selection:
|
||||
return
|
||||
name = self.listbox.get(selection)
|
||||
node_id = self.nodes[name]
|
||||
node = self.app.core.session.nodes[node_id]
|
||||
data = node.service_file_configs[self.service][self.file_name]
|
||||
canvas_node_id = self.nodes[name]
|
||||
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||
self.dialog.temp_service_files[self.file_name] = data
|
||||
self.dialog.modified_files.add(self.file_name)
|
||||
self.dialog.service_file_data.text.delete(1.0, tk.END)
|
||||
|
|
@ -83,9 +84,9 @@ class CopyServiceConfigDialog(Dialog):
|
|||
if not selection:
|
||||
return
|
||||
name = self.listbox.get(selection)
|
||||
node_id = self.nodes[name]
|
||||
node = self.app.core.session.nodes[node_id]
|
||||
data = node.service_file_configs[self.service][self.file_name]
|
||||
canvas_node_id = self.nodes[name]
|
||||
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||
dialog = ViewConfigDialog(
|
||||
self.app, self, name, self.service, self.file_name, data
|
||||
)
|
||||
|
|
@ -112,8 +113,8 @@ class ViewConfigDialog(Dialog):
|
|||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.service_data = CodeText(self.top)
|
||||
self.service_data.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.service_data.grid(sticky="nsew", pady=PADY)
|
||||
self.service_data.text.insert(tk.END, self.data)
|
||||
self.service_data.text.config(state=tk.DISABLED)
|
||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||
button.grid(sticky=tk.EW)
|
||||
button.grid(sticky="ew")
|
||||
|
|
|
|||
|
|
@ -2,32 +2,31 @@ import logging
|
|||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, Set
|
||||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.gui import images
|
||||
from core.gui import nodeutils
|
||||
from core.gui.appconfig import ICONS_PATH, CustomNode
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ServicesSelectDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", current_services: set[str]
|
||||
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
|
||||
) -> None:
|
||||
super().__init__(app, "Node Config Services", master=master)
|
||||
super().__init__(app, "Node Services", master=master)
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
self.services: Optional[CheckboxList] = None
|
||||
self.current: Optional[ListboxScroll] = None
|
||||
self.current_services: set[str] = current_services
|
||||
self.current_services: Set[str] = current_services
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
@ -35,58 +34,58 @@ class ServicesSelectDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.LabelFrame(self.top)
|
||||
frame.grid(stick=tk.NSEW, pady=PADY)
|
||||
frame.grid(stick="nsew", pady=PADY)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky=tk.NSEW)
|
||||
for group in sorted(self.app.core.config_services_groups):
|
||||
self.groups.grid(sticky="nsew")
|
||||
for group in sorted(self.app.core.services):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky=tk.NSEW)
|
||||
self.services.grid(sticky="nsew")
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky=tk.NSEW)
|
||||
self.current.grid(sticky="nsew")
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick=tk.EW)
|
||||
frame.grid(stick="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.destroy)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
# trigger group change
|
||||
self.handle_group_change()
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event: tk.Event = None) -> None:
|
||||
def handle_group_change(self, event: tk.Event) -> None:
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.config_services_groups[group]):
|
||||
for name in sorted(self.app.core.services[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
|
|
@ -114,7 +113,7 @@ class CustomNodesDialog(Dialog):
|
|||
self.image_button: Optional[ttk.Button] = None
|
||||
self.image: Optional[PhotoImage] = None
|
||||
self.image_file: Optional[str] = None
|
||||
self.services: set[str] = set()
|
||||
self.services: Set[str] = set()
|
||||
self.selected: Optional[str] = None
|
||||
self.selected_index: Optional[int] = None
|
||||
self.draw()
|
||||
|
|
@ -128,58 +127,58 @@ class CustomNodesDialog(Dialog):
|
|||
|
||||
def draw_node_config(self) -> None:
|
||||
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.nodes_list = ListboxScroll(frame)
|
||||
self.nodes_list.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX)
|
||||
self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX)
|
||||
self.nodes_list.listbox.bind("<<ListboxSelect>>", self.handle_node_select)
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
|
||||
frame = ttk.Frame(frame)
|
||||
frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
frame.grid(row=0, column=2, sticky="nsew")
|
||||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(sticky=tk.EW, pady=PADY)
|
||||
entry.grid(sticky="ew", pady=PADY)
|
||||
self.image_button = ttk.Button(
|
||||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
self.image_button.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(frame, text="Config Services", command=self.click_services)
|
||||
button.grid(sticky=tk.EW)
|
||||
self.image_button.grid(sticky="ew", pady=PADY)
|
||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||
button.grid(sticky="ew")
|
||||
|
||||
def draw_node_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
self.edit_button = ttk.Button(
|
||||
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
|
||||
)
|
||||
self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def reset_values(self) -> None:
|
||||
self.name.set("")
|
||||
|
|
@ -191,13 +190,13 @@ class CustomNodesDialog(Dialog):
|
|||
def click_icon(self) -> None:
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
image = images.from_file(file_path, width=images.NODE_SIZE)
|
||||
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
self.image = image
|
||||
self.image_file = file_path
|
||||
self.image_button.config(image=self.image)
|
||||
|
||||
def click_services(self) -> None:
|
||||
dialog = ServicesSelectDialog(self, self.app, set(self.services))
|
||||
dialog = ServicesSelectDialog(self, self.app, self.services)
|
||||
dialog.show()
|
||||
if dialog.current_services is not None:
|
||||
self.services.clear()
|
||||
|
|
@ -211,17 +210,17 @@ class CustomNodesDialog(Dialog):
|
|||
name, node_draw.image_file, list(node_draw.services)
|
||||
)
|
||||
self.app.guiconfig.nodes.append(custom_node)
|
||||
logger.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
||||
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
||||
self.app.save_config()
|
||||
self.destroy()
|
||||
|
||||
def click_create(self) -> None:
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_nodes:
|
||||
image_file = str(Path(self.image_file).absolute())
|
||||
image_file = Path(self.image_file).stem
|
||||
custom_node = CustomNode(name, image_file, list(self.services))
|
||||
node_draw = NodeDraw.from_custom(custom_node)
|
||||
logger.info(
|
||||
logging.info(
|
||||
"created new custom node (%s), image file (%s), services: (%s)",
|
||||
name,
|
||||
image_file,
|
||||
|
|
@ -238,14 +237,14 @@ class CustomNodesDialog(Dialog):
|
|||
self.selected = name
|
||||
node_draw = self.app.core.custom_nodes.pop(previous_name)
|
||||
node_draw.model = name
|
||||
node_draw.image_file = str(Path(self.image_file).absolute())
|
||||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image = self.image
|
||||
node_draw.services = set(self.services)
|
||||
logger.debug(
|
||||
node_draw.services = self.services
|
||||
logging.debug(
|
||||
"edit custom node (%s), image: (%s), services (%s)",
|
||||
node_draw.model,
|
||||
node_draw.image_file,
|
||||
node_draw.services,
|
||||
name,
|
||||
self.image_file,
|
||||
self.services,
|
||||
)
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import DIALOG_PAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -26,12 +25,12 @@ class Dialog(tk.Toplevel):
|
|||
self.modal: bool = modal
|
||||
self.title(title)
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
self.tk.call("wm", "iconphoto", self._w, image)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
|
||||
self.top.grid(sticky=tk.NSEW)
|
||||
self.top.grid(sticky="nsew")
|
||||
|
||||
def show(self) -> None:
|
||||
self.transient(self.master)
|
||||
|
|
@ -45,6 +44,6 @@ class Dialog(tk.Toplevel):
|
|||
|
||||
def draw_spacer(self, row: int = None) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=row, sticky=tk.NSEW)
|
||||
frame.grid(row=row, sticky="nsew")
|
||||
frame.rowconfigure(0, weight=1)
|
||||
self.top.rowconfigure(frame.grid_info()["row"], weight=1)
|
||||
|
|
|
|||
|
|
@ -4,19 +4,54 @@ emane configuration
|
|||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.wrappers import ConfigOption, Node
|
||||
from core.gui import images
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class GlobalEmaneDialog(Dialog):
|
||||
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||
super().__init__(app, "EMANE Configuration", master=master)
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.enabled: bool = not self.app.core.is_runtime()
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(
|
||||
self.top, self.app, self.app.core.emane_config, self.enabled
|
||||
)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneModelDialog(Dialog):
|
||||
|
|
@ -24,26 +59,29 @@ class EmaneModelDialog(Dialog):
|
|||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
node: Node,
|
||||
canvas_node: "CanvasNode",
|
||||
model: str,
|
||||
iface_id: int = None,
|
||||
) -> None:
|
||||
super().__init__(app, f"{node.name} {model} Configuration", master=master)
|
||||
self.node: Node = node
|
||||
super().__init__(
|
||||
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
|
||||
)
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
self.model: str = f"emane_{model}"
|
||||
self.iface_id: int = iface_id
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.enabled: bool = not self.app.core.is_runtime()
|
||||
self.has_error: bool = False
|
||||
try:
|
||||
config = self.node.emane_model_configs.get((self.model, self.iface_id))
|
||||
if not config:
|
||||
config = self.node.emane_model_configs.get((self.model, None))
|
||||
config = self.canvas_node.emane_model_configs.get(
|
||||
(self.model, self.iface_id)
|
||||
)
|
||||
if not config:
|
||||
config = self.app.core.get_emane_model_config(
|
||||
self.node.id, self.model, self.iface_id
|
||||
)
|
||||
self.config: dict[str, ConfigOption] = config
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
self.draw()
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get EMANE Config Error", e)
|
||||
|
|
@ -55,34 +93,36 @@ class EmaneModelDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
key = (self.model, self.iface_id)
|
||||
self.node.emane_model_configs[key] = self.config
|
||||
self.canvas_node.emane_model_configs[key] = self.config
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneConfigDialog(Dialog):
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
super().__init__(app, f"{node.name} EMANE Configuration")
|
||||
self.node: Node = node
|
||||
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(1)
|
||||
self.emane_models: list[str] = [
|
||||
self.emane_models: List[str] = [
|
||||
x.split("_")[1] for x in self.app.core.emane_models
|
||||
]
|
||||
model = self.node.emane.split("_")[1]
|
||||
|
|
@ -112,7 +152,7 @@ class EmaneConfigDialog(Dialog):
|
|||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
self.top,
|
||||
image=image,
|
||||
|
|
@ -123,32 +163,34 @@ class EmaneConfigDialog(Dialog):
|
|||
),
|
||||
)
|
||||
button.image = image
|
||||
button.grid(sticky=tk.EW, pady=PADY)
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
|
||||
def draw_emane_models(self) -> None:
|
||||
"""
|
||||
create a combobox that has all the known emane models
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Model")
|
||||
label.grid(row=0, column=0, sticky=tk.W)
|
||||
label.grid(row=0, column=0, sticky="w")
|
||||
|
||||
# create combo box and its binding
|
||||
state = "readonly" if self.enabled else tk.DISABLED
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.emane_model, values=self.emane_models, state=state
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky=tk.EW)
|
||||
combobox.grid(row=0, column=1, sticky="ew")
|
||||
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
|
||||
|
||||
def draw_emane_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
self.emane_model_button = ttk.Button(
|
||||
frame,
|
||||
text=f"{self.emane_model.get()} options",
|
||||
|
|
@ -157,25 +199,40 @@ class EmaneConfigDialog(Dialog):
|
|||
command=self.click_model_config,
|
||||
)
|
||||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(padx=PADX, sticky=tk.EW)
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
frame,
|
||||
text="EMANE options",
|
||||
image=image,
|
||||
compound=tk.RIGHT,
|
||||
command=self.click_emane_config,
|
||||
)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def draw_apply_and_cancel(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def click_emane_config(self) -> None:
|
||||
dialog = GlobalEmaneDialog(self, self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_model_config(self) -> None:
|
||||
"""
|
||||
draw emane model configuration
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
dialog = EmaneModelDialog(self, self.app, self.node, model_name)
|
||||
dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name)
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
|
||||
|
|
@ -14,13 +13,13 @@ class EmaneInstallDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
label = ttk.Label(self.top, text="EMANE needs to be installed!")
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
button = ttk.Button(
|
||||
self.top, text="EMANE Documentation", command=self.click_doc
|
||||
)
|
||||
button.grid(sticky=tk.EW, pady=PADY)
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||
button.grid(sticky=tk.EW)
|
||||
button.grid(sticky="ew")
|
||||
|
||||
def click_doc(self) -> None:
|
||||
webbrowser.open_new("https://coreemu.github.io/core/emane.html")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue