diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index dc169dcf..52440467 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -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 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index abbadab3..00000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index ca4c07dd..1a13142d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 425f2ae0..7a85ee34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +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 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 155cacc0..00000000 --- a/Dockerfile +++ /dev/null @@ -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 diff --git a/Dockerfile.centos b/Dockerfile.centos new file mode 100644 index 00000000..2096556c --- /dev/null +++ b/Dockerfile.centos @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1 +FROM centos:7 +LABEL Description="CORE Docker CentOS Image" + +# define variables +ARG PREFIX=/usr +ARG BRANCH=master + +# define environment +ENV DEBIAN_FRONTEND=noninteractive +ENV LANG en_US.UTF-8 + +# install core +RUN yum -y update && \ + yum install -y git sudo wget tzdata unzip python3 +WORKDIR /root +RUN git clone https://github.com/coreemu/core +WORKDIR /root/core +RUN git checkout ${BRANCH} +RUN ./setup.sh && python3 -m pip install --upgrade pip +RUN . /root/.bashrc && inv install -v -p ${PREFIX} +# install emane packages and python bindings +WORKDIR /root +RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.el7.x86_64.tar.gz && \ + tar xf emane-1.3.3-release-1.el7.x86_64.tar.gz && \ + cd emane-1.3.3-release-1/rpms/el7/x86_64 && \ + yum install -y epel-release && \ + yum install -y ./openstatistic*.rpm ./emane*.rpm ./python3-emane_*.rpm && \ + cd ../../../.. && \ + rm emane-1.3.3-release-1.el7.x86_64.tar.gz && \ + rm -rf emane-1.3.3-release-1 +RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip && \ + mkdir protoc && \ + unzip protoc-3.7.1-linux-x86_64.zip -d protoc +WORKDIR /root/core +RUN . /root/.bashrc && PATH=/root/protoc/bin:$PATH inv install-emane -v -e v1.3.3 + +# run daemon +CMD ["core-daemon"] diff --git a/Dockerfile.oracle b/Dockerfile.oracle new file mode 100644 index 00000000..455e5874 --- /dev/null +++ b/Dockerfile.oracle @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 +FROM oraclelinux:8 +LABEL Description="CORE Docker Oracle Linux Image" + +# define variables +ARG PREFIX=/usr +ARG BRANCH=master + +# define environment +ENV DEBIAN_FRONTEND=noninteractive +ENV LANG en_US.UTF-8 + +# install core +RUN yum -y update && \ + yum install -y git sudo wget tzdata unzip python39 which iproute-tc hostname xterm +WORKDIR /root +RUN git clone https://github.com/coreemu/core +WORKDIR /root/core +RUN git checkout ${BRANCH} +RUN PYTHON=python3.9 PYTHON_DEP=python39 ./setup.sh +RUN . /root/.bashrc && PYTHON=python3.9 PYTHON_DEP=python39 inv install -v -p ${PREFIX} +# install emane packages and python bindings +WORKDIR /root +RUN yum config-manager --set-enabled ol8_codeready_builder && yum install -y protobuf-devel +RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.el8.x86_64.tar.gz && \ + tar xf emane-1.3.3-release-1.el8.x86_64.tar.gz && \ + cd emane-1.3.3-release-1/rpms/el8/x86_64 && \ + yum install -y epel-release && \ + yum install -y ./openstatistic*.rpm ./emane*.rpm ./python3-emane-1.3.3*.rpm && \ + cd ../../../.. && \ + rm emane-1.3.3-release-1.el8.x86_64.tar.gz && \ + rm -rf emane-1.3.3-release-1 +WORKDIR /root/core +RUN . /root/.bashrc && PYTHON=python3.9 PATH=/root/protoc/bin:$PATH inv install-emane -v -e v1.3.3 + +# run daemon +CMD ["core-daemon"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu new file mode 100644 index 00000000..32dcecf6 --- /dev/null +++ b/Dockerfile.ubuntu @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1 +FROM ubuntu:20.04 +LABEL Description="CORE Docker Ubuntu Image" + +# define variables +ARG PREFIX=/usr/local +ARG BRANCH=master + +# define environment +ENV DEBIAN_FRONTEND=noninteractive + +# install core +RUN apt-get update && \ + apt-get install -y git sudo wget tzdata +WORKDIR /root +RUN git clone https://github.com/coreemu/core +WORKDIR /root/core +RUN git checkout ${BRANCH} +RUN ./setup.sh +RUN . /root/.bashrc && inv install -v -p ${PREFIX} +# install emane packages and python bindings +WORKDIR /root +RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.ubuntu-20_04.amd64.tar.gz && \ + tar xf emane*.tar.gz && \ + cd emane-1.3.3-release-1/debs/ubuntu-20_04/amd64 && \ + apt-get install -y ./emane*.deb ./python3-emane_*.deb && \ + cd ../../../.. && \ + rm emane-1.3.3-release-1.ubuntu-20_04.amd64.tar.gz && \ + rm -rf emane-1.3.3-release-1 +WORKDIR /root/core +RUN . /root/.bashrc && inv install-emane -v -e v1.3.3 + +# run daemon +CMD ["core-daemon"] diff --git a/README.md b/README.md index efab2e70..8dbe4e56 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,3 @@ -# Index -- CORE -- Docker Setup - - Precompiled container image - - Build container image from source - - Adding extra packages - -- Useful commands -- License - # CORE CORE: Common Open Research Emulator @@ -16,92 +6,40 @@ Copyright (c)2005-2022 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 +## Quick Start -```bash - -# 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 +The following should get you up and running on Ubuntu 18+ and CentOS 7+ +from a clean install, it will prompt you for sudo password. This would +install CORE into a python3 virtual environment and install +[OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source. +For more detailed installation see [here](https://coreemu.github.io/core/install.html). +```shell +git clone https://github.com/coreemu/core.git +cd core +# install dependencies to run installation task +./setup.sh +# run the following or open a new terminal +source ~/.bashrc +# Ubuntu +inv install +# CentOS +inv install -p /usr ``` -### Adding extra packages +## Documentation & Support -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. +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. -# 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) diff --git a/configure.ac b/configure.ac index 4e56507a..e71b29cf 100644 --- a/configure.ac +++ b/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, 9.0.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) @@ -83,7 +83,7 @@ if test "x$enable_daemon" = "xyes"; then want_python=yes want_linux_netns=yes - AM_PATH_PYTHON(3.9) + 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) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 2a5a1d44..4ea3332a 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -4,11 +4,10 @@ gRpc client for interfacing with CORE. import logging import threading -from collections.abc import Callable, Generator, Iterable from contextlib import contextmanager from pathlib import Path from queue import Queue -from typing import Any, Optional +from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple import grpc @@ -161,12 +160,12 @@ def throughput_listener( stream: Any, handler: Callable[[wrappers.ThroughputsEvent], None] ) -> None: """ - Listen for throughput events and provide them to the handler. + Listen for throughput events and provide them to the handler. - :param stream: grpc stream that will provide events - :param handler: function that handles an event - :return: nothing - """ + :param stream: grpc stream that will provide events + :param handler: function that handles an event + :return: nothing + """ try: for event_proto in stream: event = wrappers.ThroughputsEvent.from_proto(event_proto) @@ -236,7 +235,7 @@ class CoreGrpcClient: def start_session( self, session: wrappers.Session, definition: bool = False - ) -> tuple[bool, list[str]]: + ) -> Tuple[bool, List[str]]: """ Start a session. @@ -286,7 +285,7 @@ class CoreGrpcClient: response = self.stub.DeleteSession(request) return response.result - def get_sessions(self) -> list[wrappers.SessionSummary]: + def get_sessions(self) -> List[wrappers.SessionSummary]: """ Retrieves all currently known sessions. @@ -355,7 +354,7 @@ class CoreGrpcClient: self, session_id: int, handler: Callable[[wrappers.Event], None], - events: list[wrappers.EventType] = None, + events: List[wrappers.EventType] = None, ) -> grpc.Future: """ Listen for session events. @@ -429,7 +428,7 @@ class CoreGrpcClient: def get_node( self, session_id: int, node_id: int - ) -> tuple[wrappers.Node, list[wrappers.Interface], list[wrappers.Link]]: + ) -> Tuple[wrappers.Node, List[wrappers.Interface], List[wrappers.Link]]: """ Get node details. @@ -537,7 +536,7 @@ class CoreGrpcClient: command: str, wait: bool = True, shell: bool = False, - ) -> tuple[int, str]: + ) -> Tuple[int, str]: """ Send command to a node and get the output. @@ -576,7 +575,7 @@ class CoreGrpcClient: def add_link( self, session_id: int, link: wrappers.Link, source: str = None - ) -> tuple[bool, wrappers.Interface, wrappers.Interface]: + ) -> Tuple[bool, wrappers.Interface, wrappers.Interface]: """ Add a link between nodes. @@ -647,7 +646,7 @@ class CoreGrpcClient: def get_mobility_config( self, session_id: int, node_id: int - ) -> dict[str, wrappers.ConfigOption]: + ) -> Dict[str, wrappers.ConfigOption]: """ Get mobility configuration for a node. @@ -661,7 +660,7 @@ class CoreGrpcClient: return wrappers.ConfigOption.from_dict(response.config) def set_mobility_config( - self, session_id: int, node_id: int, config: dict[str, str] + self, session_id: int, node_id: int, config: Dict[str, str] ) -> bool: """ Set mobility configuration for a node. @@ -707,7 +706,7 @@ class CoreGrpcClient: response = self.stub.GetConfig(request) return wrappers.CoreConfig.from_proto(response) - def get_service_defaults(self, session_id: int) -> list[wrappers.ServiceDefault]: + def get_service_defaults(self, session_id: int) -> List[wrappers.ServiceDefault]: """ Get default services for different default node models. @@ -724,7 +723,7 @@ class CoreGrpcClient: return defaults def set_service_defaults( - self, session_id: int, service_defaults: dict[str, list[str]] + self, session_id: int, service_defaults: Dict[str, List[str]] ) -> bool: """ Set default services for node models. @@ -830,7 +829,7 @@ class CoreGrpcClient: def get_wlan_config( self, session_id: int, node_id: int - ) -> dict[str, wrappers.ConfigOption]: + ) -> Dict[str, wrappers.ConfigOption]: """ Get wlan configuration for a node. @@ -844,7 +843,7 @@ class CoreGrpcClient: return wrappers.ConfigOption.from_dict(response.config) def set_wlan_config( - self, session_id: int, node_id: int, config: dict[str, str] + self, session_id: int, node_id: int, config: Dict[str, str] ) -> bool: """ Set wlan configuration for a node. @@ -862,7 +861,7 @@ class CoreGrpcClient: def get_emane_model_config( self, session_id: int, node_id: int, model: str, iface_id: int = -1 - ) -> dict[str, wrappers.ConfigOption]: + ) -> Dict[str, wrappers.ConfigOption]: """ Get emane model configuration for a node or a node's interface. @@ -910,7 +909,7 @@ class CoreGrpcClient: with open(file_path, "w") as xml_file: xml_file.write(response.data) - def open_xml(self, file_path: Path, start: bool = False) -> tuple[bool, int]: + def open_xml(self, file_path: Path, start: bool = False) -> Tuple[bool, int]: """ Load a local scenario XML file to open as a new session. @@ -941,7 +940,7 @@ class CoreGrpcClient: response = self.stub.EmaneLink(request) return response.result - def get_ifaces(self) -> list[str]: + def get_ifaces(self) -> List[str]: """ Retrieves a list of interfaces available on the host machine that are not a part of a CORE session. @@ -952,26 +951,20 @@ class CoreGrpcClient: response = self.stub.GetInterfaces(request) return list(response.ifaces) - def get_config_service_defaults( - self, session_id: int, node_id: int, name: str - ) -> wrappers.ConfigServiceDefaults: + def get_config_service_defaults(self, name: str) -> wrappers.ConfigServiceDefaults: """ Retrieves config service default values. - :param session_id: session id to get node from - :param node_id: node id to get service data from :param name: name of service to get defaults for :return: config service defaults """ - request = GetConfigServiceDefaultsRequest( - name=name, session_id=session_id, node_id=node_id - ) + request = GetConfigServiceDefaultsRequest(name=name) response = self.stub.GetConfigServiceDefaults(request) return wrappers.ConfigServiceDefaults.from_proto(response) def get_node_config_service( self, session_id: int, node_id: int, name: str - ) -> dict[str, str]: + ) -> Dict[str, str]: """ Retrieves information for a specific config service on a node. @@ -989,7 +982,7 @@ class CoreGrpcClient: def get_config_service_rendered( self, session_id: int, node_id: int, name: str - ) -> dict[str, str]: + ) -> Dict[str, str]: """ Retrieve the rendered config service files for a node. @@ -1136,7 +1129,7 @@ class CoreGrpcClient: def get_wireless_config( self, session_id: int, node_id: int - ) -> dict[str, wrappers.ConfigOption]: + ) -> Dict[str, wrappers.ConfigOption]: request = GetWirelessConfigRequest(session_id=session_id, node_id=node_id) response = self.stub.GetWirelessConfig(request) return wrappers.ConfigOption.from_dict(response.config) @@ -1163,7 +1156,7 @@ class CoreGrpcClient: self.channel = None @contextmanager - def context_connect(self) -> Generator[None, None, None]: + def context_connect(self) -> Generator: """ Makes a context manager based connection to the server, will close after context ends. diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 65a20296..1a716364 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -1,9 +1,8 @@ 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 import core_pb2 from core.api.grpc.grpcutils import convert_link_data from core.emulator.data import ( ConfigData, @@ -18,18 +17,28 @@ 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.model, + icon=node.icon, + 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) @@ -180,7 +189,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): diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index f89144e4..7f63079c 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,7 +1,7 @@ import logging import time from pathlib import Path -from typing import Any, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Type, Union import grpc from grpc import ServicerContext @@ -34,9 +34,8 @@ from core.nodes.base import ( ) from core.nodes.docker import DockerNode, DockerOptions from core.nodes.interface import CoreInterface -from core.nodes.lxd import LxcNode, LxcOptions +from core.nodes.lxd import LxcNode 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 @@ -64,8 +63,8 @@ class CpuUsage: def add_node_data( - _class: type[NodeBase], node_proto: core_pb2.Node -) -> tuple[Position, NodeOptions]: + _class: Type[NodeBase], node_proto: core_pb2.Node +) -> Tuple[Position, NodeOptions]: """ Convert node protobuf message to data for creating a node. @@ -82,7 +81,7 @@ def add_node_data( options.config_services = node_proto.config_services if isinstance(options, EmaneOptions): options.emane_model = node_proto.emane - if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)): + if isinstance(options, DockerOptions): options.image = node_proto.image position = Position() position.set(node_proto.position.x, node_proto.position.y) @@ -118,8 +117,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]: """ Convert link proto to link interfaces and options data. @@ -146,8 +145,8 @@ def add_link_data( 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. @@ -177,8 +176,8 @@ def create_nodes( 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. @@ -201,8 +200,8 @@ def create_links( 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. @@ -236,7 +235,7 @@ def convert_value(value: Any) -> str: return value -def convert_session_options(session: Session) -> dict[str, common_pb2.ConfigOption]: +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) @@ -253,9 +252,9 @@ def convert_session_options(session: Session) -> dict[str, common_pb2.ConfigOpti 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. @@ -284,7 +283,7 @@ def get_config_options( def get_node_proto( - session: Session, node: NodeBase, emane_configs: list[NodeEmaneConfig] + session: Session, node: NodeBase, emane_configs: List[NodeEmaneConfig] ) -> core_pb2.Node: """ Convert CORE node to protobuf representation. @@ -314,7 +313,7 @@ def get_node_proto( if isinstance(node, EmaneNet): emane_model = node.wireless_model.name image = None - if isinstance(node, (DockerNode, LxcNode, PodmanNode)): + if isinstance(node, (DockerNode, LxcNode)): image = node.image # check for wlan config wlan_config = session.mobility.get_configs( @@ -391,7 +390,7 @@ def get_node_proto( ) -def get_links(session: Session, node: NodeBase) -> list[core_pb2.Link]: +def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]: """ Retrieve a list of links for grpc to use. @@ -436,7 +435,7 @@ def convert_iface(iface: CoreInterface) -> core_pb2.Interface: ) -def convert_core_link(core_link: CoreLink) -> list[core_pb2.Link]: +def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]: """ Convert core link to protobuf data. @@ -582,33 +581,25 @@ def convert_link( ) -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,11 +658,10 @@ def get_service_configuration(service: CoreService) -> NodeServiceData: ) -def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface: +def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. - :param session: session interface belongs to :param iface: interface to convert :return: interface proto """ @@ -682,11 +672,6 @@ 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, name=iface.name, @@ -697,8 +682,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, ) @@ -729,7 +712,7 @@ def get_nem_id( return nem_id -def get_emane_model_configs_dict(session: Session) -> dict[int, list[NodeEmaneConfig]]: +def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: """ Get emane model configuration protobuf data. @@ -752,7 +735,7 @@ def get_emane_model_configs_dict(session: Session) -> dict[int, list[NodeEmaneCo return configs -def get_hooks(session: Session) -> list[core_pb2.Hook]: +def get_hooks(session: Session) -> List[core_pb2.Hook]: """ Retrieve hook protobuf data for a session. @@ -768,7 +751,7 @@ def get_hooks(session: Session) -> list[core_pb2.Hook]: return hooks -def get_default_services(session: Session) -> list[ServiceDefaults]: +def get_default_services(session: Session) -> List[ServiceDefaults]: """ Retrieve the default service sets for a given session. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 6a86ab0a..532dd91c 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1,15 +1,12 @@ +import atexit import logging import os import re -import signal -import sys import tempfile import time -from collections.abc import Iterable from concurrent import futures from pathlib import Path -from re import Pattern -from typing import Optional +from typing import Iterable, Optional, Pattern, Type import grpc from grpc import ServicerContext @@ -107,7 +104,7 @@ from core.services.coreservices import ServiceManager logger = logging.getLogger(__name__) _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 -_INTERFACE_REGEX: Pattern[str] = re.compile(r"beth(?P[0-9a-fA-F]+)") +_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P[0-9a-fA-F]+)") _MAX_WORKERS = 1000 @@ -123,20 +120,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): self.coreemu: CoreEmu = coreemu self.running: bool = True self.server: Optional[grpc.Server] = None - # catch signals - signal.signal(signal.SIGHUP, self._signal_handler) - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) - signal.signal(signal.SIGUSR1, self._signal_handler) - signal.signal(signal.SIGUSR2, self._signal_handler) + atexit.register(self._exit_handler) - def _signal_handler(self, signal_number: int, _) -> None: - logger.info("caught signal: %s", signal_number) - self.coreemu.shutdown() + def _exit_handler(self) -> None: + logger.debug("catching exit, stop running") self.running = False - if self.server: - self.server.stop(None) - sys.exit(signal_number) def _is_running(self, context) -> bool: return self.running and context.is_active() @@ -173,7 +161,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return session def get_node( - self, session: Session, node_id: int, context: ServicerContext, _class: type[NT] + self, session: Session, node_id: int, context: ServicerContext, _class: Type[NT] ) -> NT: """ Retrieve node given session and node id @@ -212,7 +200,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def validate_service( self, name: str, context: ServicerContext - ) -> type[ConfigService]: + ) -> Type[ConfigService]: """ Validates a configuration service is a valid known service. @@ -284,8 +272,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # session options for option in request.session.options.values(): - if option.value: - session.options.set(option.name, option.value) + session.options.set(option.name, option.value) session.metadata = dict(request.session.metadata) # add servers @@ -404,11 +391,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): self, request: core_pb2.GetSessionsRequest, context: ServicerContext ) -> core_pb2.GetSessionsResponse: """ - Get all currently known session overviews. + Delete the session - :param request: get sessions request + :param request: get-session request :param context: context object - :return: a get sessions response + :return: a delete-session response """ logger.debug("get sessions: %s", request) sessions = [] @@ -495,6 +482,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): while self._is_running(context): now = time.monotonic() stats = get_net_stats() + # calculate average if last_check is not None: interval = now - last_check @@ -511,7 +499,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): (current_rxtx["tx"] - previous_rxtx["tx"]) * 8.0 / interval ) throughput = rx_kbps + tx_kbps - if key.startswith("beth"): + if key.startswith("veth"): key = key.split(".") node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = int(node_id, base=16) @@ -537,6 +525,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): bridge_throughput.throughput = throughput except ValueError: pass + yield throughputs_event last_check = now @@ -596,7 +585,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ifaces = [] for iface_id in node.ifaces: iface = node.ifaces[iface_id] - iface_proto = grpcutils.iface_to_proto(session, iface) + iface_proto = grpcutils.iface_to_proto(iface) ifaces.append(iface_proto) emane_configs = grpcutils.get_emane_model_configs_dict(session) node_emane_configs = emane_configs.get(node.id, []) @@ -767,9 +756,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): iface1_proto = None iface2_proto = None if node1_iface: - iface1_proto = grpcutils.iface_to_proto(session, node1_iface) + iface1_proto = grpcutils.iface_to_proto(node1_iface) if node2_iface: - iface2_proto = grpcutils.iface_to_proto(session, node2_iface) + iface2_proto = grpcutils.iface_to_proto(node2_iface) return core_pb2.AddLinkResponse( result=True, iface1=iface1_proto, iface2=iface2_proto ) @@ -1106,7 +1095,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_id = request.wlan_config.node_id config = request.wlan_config.config session.mobility.set_model_config(node_id, BasicRangeModel.name, config) - if session.is_running(): + if session.state == EventTypes.RUNTIME_STATE: node = self.get_node(session, node_id, context, WlanNode) node.updatemodel(config) return SetWlanConfigResponse(result=True) @@ -1179,7 +1168,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logger.debug("open xml: %s", request) session = self.coreemu.create_session() temp = tempfile.NamedTemporaryFile(delete=False) - temp.write(request.data.encode()) + temp.write(request.data.encode("utf-8")) temp.close() temp_path = Path(temp.name) file_path = Path(request.file) @@ -1285,10 +1274,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: grpc context :return: get config service defaults response """ - session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, CoreNode) service_class = self.validate_service(request.name, context) - service = service_class(node) + service = service_class(None) templates = service.get_templates() config = {} for configuration in service.default_configs: diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index f84e6a08..a49b9932 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from enum import Enum from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, List, Optional, Set, Tuple from core.api.grpc import ( common_pb2, @@ -68,7 +68,6 @@ class NodeType(Enum): DOCKER = 15 LXC = 16 WIRELESS = 17 - PODMAN = 18 class LinkType(Enum): @@ -115,13 +114,13 @@ class EventType: class ConfigService: group: str name: str - executables: list[str] - dependencies: list[str] - directories: list[str] - files: list[str] - startup: list[str] - validate: list[str] - shutdown: list[str] + executables: List[str] + dependencies: List[str] + directories: List[str] + files: List[str] + startup: List[str] + validate: List[str] + shutdown: List[str] validation_mode: ConfigServiceValidationMode validation_timer: int validation_period: float @@ -148,8 +147,8 @@ class ConfigService: class ConfigServiceConfig: node_id: int name: str - templates: dict[str, str] - config: dict[str, str] + templates: Dict[str, str] + config: Dict[str, str] @classmethod def from_proto( @@ -165,15 +164,15 @@ class ConfigServiceConfig: @dataclass class ConfigServiceData: - templates: dict[str, str] = field(default_factory=dict) - config: dict[str, str] = field(default_factory=dict) + templates: Dict[str, str] = field(default_factory=dict) + config: Dict[str, str] = field(default_factory=dict) @dataclass class ConfigServiceDefaults: - templates: dict[str, str] - config: dict[str, "ConfigOption"] - modes: dict[str, dict[str, str]] + templates: Dict[str, str] + config: Dict[str, "ConfigOption"] + modes: Dict[str, Dict[str, str]] @classmethod def from_proto( @@ -212,7 +211,7 @@ class Service: @dataclass class ServiceDefault: model: str - services: list[str] + services: List[str] @classmethod def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault": @@ -221,15 +220,15 @@ class ServiceDefault: @dataclass class NodeServiceData: - executables: list[str] = field(default_factory=list) - dependencies: list[str] = field(default_factory=list) - dirs: list[str] = field(default_factory=list) - configs: list[str] = field(default_factory=list) - startup: list[str] = field(default_factory=list) - validate: list[str] = field(default_factory=list) + executables: List[str] = field(default_factory=list) + dependencies: List[str] = field(default_factory=list) + dirs: List[str] = field(default_factory=list) + configs: List[str] = field(default_factory=list) + startup: List[str] = field(default_factory=list) + validate: List[str] = field(default_factory=list) validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING validation_timer: int = 5 - shutdown: list[str] = field(default_factory=list) + shutdown: List[str] = field(default_factory=list) meta: str = None @classmethod @@ -267,7 +266,7 @@ class NodeServiceConfig: node_id: int service: str data: NodeServiceData - files: dict[str, str] = field(default_factory=dict) + files: Dict[str, str] = field(default_factory=dict) @classmethod def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig": @@ -283,11 +282,11 @@ class NodeServiceConfig: class ServiceConfig: node_id: int service: str - files: list[str] = None - directories: list[str] = None - startup: list[str] = None - validate: list[str] = None - shutdown: list[str] = None + files: List[str] = None + directories: List[str] = None + startup: List[str] = None + validate: List[str] = None + shutdown: List[str] = None def to_proto(self) -> services_pb2.ServiceConfig: return services_pb2.ServiceConfig( @@ -340,8 +339,8 @@ class InterfaceThroughput: @dataclass class ThroughputsEvent: session_id: int - bridge_throughputs: list[BridgeThroughput] - iface_throughputs: list[InterfaceThroughput] + bridge_throughputs: List[BridgeThroughput] + iface_throughputs: List[InterfaceThroughput] @classmethod def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent": @@ -429,19 +428,19 @@ class ConfigOption: label: str = None type: ConfigOptionType = None group: str = None - select: list[str] = None + select: List[str] = None @classmethod def from_dict( - cls, config: dict[str, common_pb2.ConfigOption] - ) -> dict[str, "ConfigOption"]: + cls, config: Dict[str, common_pb2.ConfigOption] + ) -> Dict[str, "ConfigOption"]: d = {} for key, value in config.items(): d[key] = ConfigOption.from_proto(value) return d @classmethod - def to_dict(cls, config: dict[str, "ConfigOption"]) -> dict[str, str]: + def to_dict(cls, config: Dict[str, "ConfigOption"]) -> Dict[str, str]: return {k: v.value for k, v in config.items()} @classmethod @@ -482,8 +481,6 @@ class Interface: mtu: int = None node_id: int = None net2_id: int = None - nem_id: int = None - nem_port: int = None @classmethod def from_proto(cls, proto: core_pb2.Interface) -> "Interface": @@ -500,8 +497,6 @@ class Interface: mtu=proto.mtu, node_id=proto.node_id, net2_id=proto.net2_id, - nem_id=proto.nem_id, - nem_port=proto.nem_port, ) def to_proto(self) -> core_pb2.Interface: @@ -672,7 +667,7 @@ class EmaneModelConfig: node_id: int model: str iface_id: int = -1 - config: dict[str, ConfigOption] = None + config: Dict[str, ConfigOption] = None @classmethod def from_proto(cls, proto: emane_pb2.GetEmaneModelConfig) -> "EmaneModelConfig": @@ -726,8 +721,8 @@ class Node: type: NodeType = NodeType.DEFAULT model: str = None position: Position = Position(x=0, y=0) - services: set[str] = field(default_factory=set) - config_services: set[str] = field(default_factory=set) + services: Set[str] = field(default_factory=set) + config_services: Set[str] = field(default_factory=set) emane: str = None icon: str = None image: str = None @@ -738,19 +733,19 @@ class Node: canvas: int = None # configurations - emane_model_configs: dict[ - tuple[str, Optional[int]], dict[str, ConfigOption] + emane_model_configs: Dict[ + Tuple[str, Optional[int]], Dict[str, ConfigOption] ] = field(default_factory=dict, repr=False) - wlan_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False) - wireless_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False) - mobility_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False) - service_configs: dict[str, NodeServiceData] = field( + wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + wireless_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + service_configs: Dict[str, NodeServiceData] = field( default_factory=dict, repr=False ) - service_file_configs: dict[str, dict[str, str]] = field( + service_file_configs: Dict[str, Dict[str, str]] = field( default_factory=dict, repr=False ) - config_service_configs: dict[str, ConfigServiceData] = field( + config_service_configs: Dict[str, ConfigServiceData] = field( default_factory=dict, repr=False ) @@ -777,7 +772,7 @@ class Node: id=proto.id, name=proto.name, type=NodeType(proto.type), - model=proto.model or None, + model=proto.model, position=Position.from_proto(proto.position), services=set(proto.services), config_services=set(proto.config_services), @@ -850,18 +845,18 @@ class Node: wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()}, ) - def set_wlan(self, config: dict[str, str]) -> None: + def set_wlan(self, config: Dict[str, str]) -> None: for key, value in config.items(): option = ConfigOption(name=key, value=value) self.wlan_config[key] = option - def set_mobility(self, config: dict[str, str]) -> None: + def set_mobility(self, config: Dict[str, str]) -> None: for key, value in config.items(): option = ConfigOption(name=key, value=value) self.mobility_config[key] = option def set_emane_model( - self, model: str, config: dict[str, str], iface_id: int = None + self, model: str, config: Dict[str, str], iface_id: int = None ) -> None: key = (model, iface_id) config_options = self.emane_model_configs.setdefault(key, {}) @@ -874,23 +869,23 @@ class Node: class Session: id: int = None state: SessionState = SessionState.DEFINITION - nodes: dict[int, Node] = field(default_factory=dict) - links: list[Link] = field(default_factory=list) + nodes: Dict[int, Node] = field(default_factory=dict) + links: List[Link] = field(default_factory=list) dir: str = None user: str = None - default_services: dict[str, set[str]] = field(default_factory=dict) + default_services: Dict[str, Set[str]] = field(default_factory=dict) location: SessionLocation = SessionLocation( x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0 ) - hooks: dict[str, Hook] = field(default_factory=dict) - metadata: dict[str, str] = field(default_factory=dict) + hooks: Dict[str, Hook] = field(default_factory=dict) + metadata: Dict[str, str] = field(default_factory=dict) file: Path = None - options: dict[str, ConfigOption] = field(default_factory=dict) - servers: list[Server] = field(default_factory=list) + options: Dict[str, ConfigOption] = field(default_factory=dict) + servers: List[Server] = field(default_factory=list) @classmethod def from_proto(cls, proto: core_pb2.Session) -> "Session": - nodes: dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} + nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} links = [Link.from_proto(x) for x in proto.links] default_services = {x.model: set(x.services) for x in proto.default_services} hooks = {x.file: Hook.from_proto(x) for x in proto.hooks} @@ -988,7 +983,7 @@ class Session: self.links.append(link) return link - def set_options(self, config: dict[str, str]) -> None: + def set_options(self, config: Dict[str, str]) -> None: for key, value in config.items(): option = ConfigOption(name=key, value=value) self.options[key] = option @@ -996,9 +991,9 @@ class Session: @dataclass class CoreConfig: - services: list[Service] = field(default_factory=list) - config_services: list[ConfigService] = field(default_factory=list) - emane_models: list[str] = field(default_factory=list) + services: List[Service] = field(default_factory=list) + config_services: List[ConfigService] = field(default_factory=list) + emane_models: List[str] = field(default_factory=list) @classmethod def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig": @@ -1089,7 +1084,7 @@ class ConfigEvent: node_id: int object: str type: int - data_types: list[int] + data_types: List[int] data_values: str captions: str bitmap: str diff --git a/daemon/core/config.py b/daemon/core/config.py index 7a6ffa49..ae40627e 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -5,7 +5,7 @@ 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, Set, Tuple, Type, Union from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes @@ -17,9 +17,9 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.location.mobility import WirelessModel - WirelessModelType = type[WirelessModel] + WirelessModelType = Type[WirelessModel] -_BOOL_OPTIONS: set[str] = {"0", "1"} +_BOOL_OPTIONS: Set[str] = {"0", "1"} @dataclass @@ -43,7 +43,7 @@ class Configuration: type: ConfigDataTypes label: str = None default: str = "" - options: list[str] = field(default_factory=list) + options: List[str] = field(default_factory=list) group: str = "Configuration" def __post_init__(self) -> None: @@ -118,10 +118,10 @@ class ConfigurableOptions: """ name: Optional[str] = None - options: list[Configuration] = [] + options: List[Configuration] = [] @classmethod - def configurations(cls) -> list[Configuration]: + def configurations(cls) -> List[Configuration]: """ Provides the configurations for this class. @@ -130,7 +130,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 +139,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 +165,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 +208,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: @@ -250,7 +250,7 @@ class ConfigurableManager: def get_configs( self, node_id: int = _default_node, config_type: str = _default_type - ) -> Optional[dict[str, str]]: + ) -> Optional[Dict[str, str]]: """ Retrieve configurations for a node and configuration type. @@ -264,7 +264,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 +284,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 +317,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 +342,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. @@ -361,7 +361,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. diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index e15260eb..3d61edcc 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -5,7 +5,7 @@ import logging import time from dataclasses import dataclass from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, List, Optional from mako import exceptions from mako.lookup import TemplateLookup @@ -67,7 +67,7 @@ class ConfigService(abc.ABC): validation_timer: int = 5 # directories to shadow and copy files from - shadow_directories: list[ShadowDir] = [] + shadow_directories: List[ShadowDir] = [] def __init__(self, node: CoreNode) -> None: """ @@ -79,9 +79,9 @@ class ConfigService(abc.ABC): class_file = inspect.getfile(self.__class__) templates_path = 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 +108,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 @@ -276,7 +276,7 @@ class ConfigService(abc.ABC): 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 +303,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. @@ -331,7 +331,7 @@ class ConfigService(abc.ABC): templates[file] = template return templates - def get_rendered_templates(self) -> dict[str, str]: + def get_rendered_templates(self) -> Dict[str, str]: templates = {} data = self.data() for file in sorted(self.files): @@ -339,7 +339,7 @@ class ConfigService(abc.ABC): templates[file] = rendered return templates - def _get_rendered_template(self, file: str, data: dict[str, Any]) -> str: + 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: @@ -426,7 +426,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 +440,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,7 +458,7 @@ 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, template_path: str, data: Dict[str, Any] = None) -> str: """ Renders file based template providing all associated data to template. @@ -475,7 +475,7 @@ class ConfigService(abc.ABC): f"{exceptions.text_error_template().render_unicode()}" ) - def _define_config(self, configs: list[Configuration]) -> None: + def _define_config(self, configs: List[Configuration]) -> None: """ Initializes default configuration data. @@ -485,7 +485,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 +496,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. diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/configservice/dependencies.py index 1fbc4e48..b24c83c6 100644 --- a/daemon/core/configservice/dependencies.py +++ b/daemon/core/configservice/dependencies.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Set logger = logging.getLogger(__name__) @@ -12,16 +12,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 +29,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. @@ -54,8 +54,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,7 +70,7 @@ 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. @@ -81,7 +81,7 @@ class ConfigServiceDependencies: 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. @@ -96,14 +96,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: diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py index 542f3cc5..1fd26e43 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/configservice/manager.py @@ -2,6 +2,7 @@ import logging import pathlib import pkgutil from pathlib import Path +from typing import Dict, List, Type from core import configservices, utils from core.configservice.base import ConfigService @@ -19,9 +20,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. @@ -34,7 +35,7 @@ class ConfigServiceManager: raise CoreError(f"service does not exist {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. @@ -61,7 +62,7 @@ class ConfigServiceManager: # make service available self.services[name] = service - def load_locals(self) -> list[str]: + def load_locals(self) -> List[str]: """ Search and add config service from local core module. @@ -80,7 +81,7 @@ class ConfigServiceManager: logger.debug("not loading config service(%s): %s", service.name, e) return errors - def load(self, path: Path) -> list[str]: + def load(self, path: Path) -> List[str]: """ Search path provided for config services and add them for being managed. diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index 378d42f8..7ed965be 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -1,5 +1,5 @@ import abc -from typing import Any +from typing import Any, Dict, List from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode @@ -82,30 +82,29 @@ def rj45_check(iface: CoreInterface) -> bool: 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] = ["bash 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", 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", default="/usr/local/sbin /usr/sbin /usr/lib/frr" ).strip('"') services = [] @@ -146,16 +145,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 +175,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: @@ -228,8 +227,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 +264,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 +294,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 +319,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 +345,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: @@ -386,8 +385,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: diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index 3002cd94..ba9ef29c 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -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] = ["bash 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] = ["bash 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,18 @@ 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] = ["bash 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_nhdp = "NHDP" in self.node.config_services has_olsr = "OLSR" in self.node.config_services ifnames = [] @@ -84,18 +84,18 @@ class NrlSmf(ConfigService): 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] = ["bash 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 +108,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] = ["bash 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 +130,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] = ["bash 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 +152,13 @@ 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] = ["bash 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]] = {} diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 8b4d4909..66afdcc1 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -1,6 +1,6 @@ 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 @@ -84,22 +84,22 @@ def rj45_check(iface: CoreInterface) -> bool: 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] = ["bash 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]: + def data(self) -> Dict[str, Any]: quagga_bin_search = self.node.session.options.get( "quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga" ).strip('"') @@ -153,16 +153,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,8 +183,8 @@ 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: @@ -234,8 +234,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 @@ -300,12 +300,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 +322,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 +329,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 +354,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 +380,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: @@ -420,8 +420,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: diff --git a/daemon/core/configservices/securityservices/services.py b/daemon/core/configservices/securityservices/services.py index e6243b2c..e866617c 100644 --- a/daemon/core/configservices/securityservices/services.py +++ b/daemon/core/configservices/securityservices/services.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Dict, List from core.config import ConfigString, Configuration from core.configservice.base import ConfigService, ConfigServiceMode @@ -9,41 +9,41 @@ 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] = ["bash vpnclient.sh"] + validate: List[str] = ["pidof openvpn"] + shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING - default_configs: list[Configuration] = [ + 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"), ] - 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] = ["bash vpnserver.sh"] + validate: List[str] = ["pidof openvpn"] + shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING - default_configs: list[Configuration] = [ + 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"), ] - 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 +56,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] = ["bash 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] = ["bash 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] = ["bash 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) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 73d72060..3a4addfe 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -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] = ["bash 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] = ["bash 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] = ["bash 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] = ["bash 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] = ["bash 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,18 +134,18 @@ 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: @@ -162,18 +162,18 @@ class DhcpService(ConfigService): 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] = ["bash 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 +183,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] = ["bash pcap.sh start"] + validate: List[str] = ["pidof tcpdump"] + shutdown: List[str] = ["bash 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", "/var/run/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 +247,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] = ["bash 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 +270,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) diff --git a/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf b/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf index d003b4b1..1436f068 100644 --- a/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf +++ b/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf @@ -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; diff --git a/daemon/core/configservices/utilservices/templates/ipforward.sh b/daemon/core/configservices/utilservices/templates/ipforward.sh index 75717ecf..a8d3abed 100644 --- a/daemon/core/configservices/utilservices/templates/ipforward.sh +++ b/daemon/core/configservices/utilservices/templates/ipforward.sh @@ -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 diff --git a/daemon/core/configservices/utilservices/templates/pcap.sh b/daemon/core/configservices/utilservices/templates/pcap.sh index d4a0ea9f..6a099f8c 100644 --- a/daemon/core/configservices/utilservices/templates/pcap.sh +++ b/daemon/core/configservices/utilservices/templates/pcap.sh @@ -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 diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index c02570c9..294ae528 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -6,7 +6,7 @@ import logging import os import threading from enum import Enum -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from core import utils from core.emane.emanemodel import EmaneModel @@ -126,9 +126,9 @@ class EmaneManager: """ super().__init__() self.session: "Session" = session - self.nems_to_ifaces: dict[int, CoreInterface] = {} - self.ifaces_to_nems: dict[CoreInterface, int] = {} - self._emane_nets: dict[int, EmaneNet] = {} + self.nems_to_ifaces: Dict[int, CoreInterface] = {} + self.ifaces_to_nems: Dict[CoreInterface, int] = {} + self._emane_nets: Dict[int, EmaneNet] = {} self._emane_node_lock: threading.Lock = threading.Lock() # port numbers are allocated from these counters self.platformport: int = self.session.options.get_int( @@ -141,14 +141,14 @@ class EmaneManager: self.eventmonthread: Optional[threading.Thread] = None # model for global EMANE configuration options - self.node_configs: dict[int, dict[str, dict[str, str]]] = {} - self.node_models: dict[int, str] = {} + self.node_configs: Dict[int, Dict[str, Dict[str, str]]] = {} + self.node_models: Dict[int, str] = {} # link monitor self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self) # emane event monitoring - self.services: dict[str, EmaneEventService] = {} - self.nem_service: dict[int, EmaneEventService] = {} + self.services: Dict[str, EmaneEventService] = {} + self.nem_service: Dict[int, EmaneEventService] = {} def next_nem_id(self, iface: CoreInterface) -> int: nem_id = self.session.options.get_int("nem_id_start") @@ -161,7 +161,7 @@ class EmaneManager: def get_config( self, key: int, model: str, default: bool = True - ) -> Optional[dict[str, str]]: + ) -> Optional[Dict[str, str]]: """ Get the current or default configuration for an emane model. @@ -181,7 +181,7 @@ class EmaneManager: config = model_class.default_values() return config - def set_config(self, key: int, model: str, config: dict[str, str] = None) -> None: + def set_config(self, key: int, model: str, config: Dict[str, str] = None) -> None: """ Sets and update the provided configuration against the default model or currently set emane model configuration. @@ -199,7 +199,7 @@ class EmaneManager: model_configs = self.node_configs.setdefault(key, {}) model_configs[model] = model_config - def get_model(self, model_name: str) -> type[EmaneModel]: + def get_model(self, model_name: str) -> Type[EmaneModel]: """ Convenience method for getting globally loaded emane models. @@ -211,7 +211,7 @@ class EmaneManager: def get_iface_config( self, emane_net: EmaneNet, iface: CoreInterface - ) -> dict[str, str]: + ) -> Dict[str, str]: """ Retrieve configuration for a given interface, first checking for interface specific config, node specific config, network specific config, and finally @@ -260,7 +260,7 @@ class EmaneManager: ) self._emane_nets[emane_net.id] = emane_net - def getnodes(self) -> set[CoreNode]: + def getnodes(self) -> Set[CoreNode]: """ Return a set of CoreNodes that are linked to an EMANE network, e.g. containers having one or more radio interfaces. @@ -335,7 +335,7 @@ class EmaneManager: self.start_daemon(iface) self.install_iface(iface, config) - def get_ifaces(self) -> list[tuple[EmaneNet, TunTap]]: + def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]: ifaces = [] for emane_net in self._emane_nets.values(): if not emane_net.wireless_model: @@ -354,7 +354,7 @@ class EmaneManager: return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id)) def setup_control_channels( - self, nem_id: int, iface: CoreInterface, config: dict[str, str] + self, nem_id: int, iface: CoreInterface, config: Dict[str, str] ) -> None: node = iface.node # setup ota device @@ -419,7 +419,7 @@ class EmaneManager: def get_nem_position( self, iface: CoreInterface - ) -> Optional[tuple[int, float, float, int]]: + ) -> Optional[Tuple[int, float, float, int]]: """ Retrieves nem position for a given interface. @@ -453,7 +453,7 @@ class EmaneManager: event.append(nemid, latitude=lat, longitude=lon, altitude=alt) self.publish_event(nemid, event, send_all=True) - def set_nem_positions(self, moved_ifaces: list[CoreInterface]) -> None: + def set_nem_positions(self, moved_ifaces: List[CoreInterface]) -> None: """ Several NEMs have moved, from e.g. a WaypointMobilityModel calculation. Generate an EMANE Location Event having several @@ -480,7 +480,7 @@ class EmaneManager: try: with path.open("a") as f: f.write(f"{iface.node.name} {iface.name} {nem_id}\n") - except OSError: + except IOError: logger.exception("error writing to emane nem file") def links_enabled(self) -> bool: @@ -624,7 +624,7 @@ class EmaneManager: args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) - def install_iface(self, iface: TunTap, config: dict[str, str]) -> None: + def install_iface(self, iface: TunTap, config: Dict[str, str]) -> None: external = config.get("external", "0") if external == "0": iface.set_ips() diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index ea2b05fd..0fb5bc17 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import Dict, List from core.config import Configuration from core.emulator.enumerations import ConfigDataTypes @@ -32,7 +33,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 +51,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,7 +74,7 @@ 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: Path, defaults: Dict[str, str]) -> List[Configuration]: """ Parses a valid emane manifest file and converts the provided configuration values into ones used by core. diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 4e31d632..cc5b0f4d 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -3,7 +3,7 @@ Defines Emane Models used within CORE. """ import logging from pathlib import Path -from typing import Optional +from typing import Dict, List, Optional, Set from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration from core.emane import emanemanifest @@ -28,38 +28,38 @@ class EmaneModel(WirelessModel): # default platform configuration settings platform_controlport: str = "controlportendpoint" platform_xml: str = "nemmanager.xml" - platform_defaults: dict[str, str] = { + 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] = [] + 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] = [ + 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"), ] - config_ignore: set[str] = set() + config_ignore: Set[str] = set() @classmethod def load(cls, emane_prefix: Path) -> None: @@ -94,7 +94,7 @@ class EmaneModel(WirelessModel): 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). @@ -105,7 +105,7 @@ class EmaneModel(WirelessModel): ) @classmethod - def config_groups(cls) -> list[ConfigGroup]: + def config_groups(cls) -> List[ConfigGroup]: """ Returns the defined configuration groups. @@ -122,7 +122,7 @@ class EmaneModel(WirelessModel): 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. @@ -146,7 +146,7 @@ class EmaneModel(WirelessModel): """ logger.debug("emane model(%s) has no post setup tasks", self.name) - def update(self, moved_ifaces: list[CoreInterface]) -> None: + def update(self, 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 diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 1997e9f8..5ed6d49d 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -2,7 +2,7 @@ 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 @@ -34,10 +34,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 @@ -84,7 +84,7 @@ class EmaneClient: self.client: shell.ControlPortClient = shell.ControlPortClient( self.address, port ) - self.nems: dict[int, LossTable] = {} + self.nems: Dict[int, LossTable] = {} self.setup() def setup(self) -> None: @@ -110,7 +110,7 @@ class EmaneClient: 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,11 +138,11 @@ 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) - 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) @@ -159,7 +159,7 @@ 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) tree = etree.parse(pcr) @@ -179,9 +179,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 @@ -210,7 +210,7 @@ class EmaneLinkMonitor: if client.nems: self.clients.append(client) - def get_addresses(self) -> list[tuple[str, int]]: + def get_addresses(self) -> List[Tuple[str, int]]: addresses = [] nodes = self.emane_manager.getnodes() for node in nodes: @@ -273,25 +273,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: diff --git a/daemon/core/emane/modelmanager.py b/daemon/core/emane/modelmanager.py index 92dd5b8e..989802c4 100644 --- a/daemon/core/emane/modelmanager.py +++ b/daemon/core/emane/modelmanager.py @@ -1,6 +1,7 @@ import logging import pkgutil from pathlib import Path +from typing import Dict, List, Type from core import utils from core.emane import models as emane_models @@ -11,10 +12,10 @@ logger = logging.getLogger(__name__) class EmaneModelManager: - models: dict[str, type[EmaneModel]] = {} + models: Dict[str, Type[EmaneModel]] = {} @classmethod - def load_locals(cls, emane_prefix: Path) -> list[str]: + def load_locals(cls, emane_prefix: Path) -> List[str]: """ Load local core emane models and make them available. @@ -37,7 +38,7 @@ class EmaneModelManager: return errors @classmethod - def load(cls, path: Path, emane_prefix: Path) -> list[str]: + def load(cls, path: Path, emane_prefix: Path) -> List[str]: """ Search and load custom emane models and make them available. @@ -62,7 +63,7 @@ class EmaneModelManager: return errors @classmethod - def get(cls, name: str) -> type[EmaneModel]: + 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}") diff --git a/daemon/core/emane/models/bypass.py b/daemon/core/emane/models/bypass.py index e8f2ed39..25841114 100644 --- a/daemon/core/emane/models/bypass.py +++ b/daemon/core/emane/models/bypass.py @@ -2,6 +2,7 @@ EMANE Bypass model for CORE """ from pathlib import Path +from typing import List, Set from core.config import ConfigBool, Configuration from core.emane import emanemodel @@ -11,11 +12,11 @@ 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] = [ + mac_config: List[Configuration] = [ ConfigBool( id="none", default="0", @@ -25,7 +26,7 @@ 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: diff --git a/daemon/core/emane/models/commeffect.py b/daemon/core/emane/models/commeffect.py index aa093a93..c3f0b07b 100644 --- a/daemon/core/emane/models/commeffect.py +++ b/daemon/core/emane/models/commeffect.py @@ -4,6 +4,7 @@ commeffect.py: EMANE CommEffect model for CORE import logging from pathlib import Path +from typing import Dict, List from lxml import etree @@ -41,12 +42,12 @@ 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: @@ -55,11 +56,11 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults) @classmethod - def configurations(cls) -> list[Configuration]: + def configurations(cls) -> List[Configuration]: return cls.platform_config + cls.config_shim @classmethod - def config_groups(cls) -> list[ConfigGroup]: + def config_groups(cls) -> List[ConfigGroup]: platform_len = len(cls.platform_config) return [ ConfigGroup("Platform Parameters", 1, platform_len), @@ -70,7 +71,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): ), ] - 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 diff --git a/daemon/core/emane/models/tdma.py b/daemon/core/emane/models/tdma.py index 100e960d..c6ac631b 100644 --- a/daemon/core/emane/models/tdma.py +++ b/daemon/core/emane/models/tdma.py @@ -4,6 +4,7 @@ tdma.py: EMANE TDMA model bindings for CORE import logging from pathlib import Path +from typing import Set from core import constants, utils from core.config import ConfigString @@ -27,7 +28,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel): default_schedule: Path = ( constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml" ) - config_ignore: set[str] = {schedule_name} + config_ignore: Set[str] = {schedule_name} @classmethod def load(cls, emane_prefix: Path) -> None: diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index ecf684d7..1a0b6e75 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -6,11 +6,11 @@ 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, Callable, Dict, List, Optional, Type, Union from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import MessageFlags, RegisterTlvs +from core.emulator.enumerations import EventTypes, MessageFlags, RegisterTlvs from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions from core.nodes.interface import CoreInterface @@ -167,7 +167,7 @@ class EmaneNet(CoreNetworkBase): 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(): + if self.session.state == EventTypes.RUNTIME_STATE: self.session.emane.add_node(self) @classmethod @@ -196,7 +196,7 @@ class EmaneNet(CoreNetworkBase): def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: pass - def updatemodel(self, config: dict[str, str]) -> None: + def updatemodel(self, config: Dict[str, str]) -> None: """ Update configuration for the current model. @@ -212,8 +212,8 @@ class EmaneNet(CoreNetworkBase): def setmodel( self, - model: Union[type["EmaneModel"], type["WayPointMobility"]], - config: dict[str, str], + model: Union[Type["EmaneModel"], Type["WayPointMobility"]], + config: Dict[str, str], ) -> None: """ set the EmaneModel associated with this node @@ -225,7 +225,7 @@ class EmaneNet(CoreNetworkBase): self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) - def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: links = [] emane_manager = self.session.emane # gather current emane links @@ -280,7 +280,7 @@ class EmaneNet(CoreNetworkBase): self.attach(iface) if self.up: iface.startup() - if self.session.is_running(): + if self.session.state == EventTypes.RUNTIME_STATE: self.session.emane.start_iface(self, iface) return iface diff --git a/daemon/core/emulator/broadcast.py b/daemon/core/emulator/broadcast.py deleted file mode 100644 index bf56f99d..00000000 --- a/daemon/core/emulator/broadcast.py +++ /dev/null @@ -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) diff --git a/daemon/core/emulator/controlnets.py b/daemon/core/emulator/controlnets.py deleted file mode 100644 index 27b00367..00000000 --- a/daemon/core/emulator/controlnets.py +++ /dev/null @@ -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" - ) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 574002e6..3a9c847b 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -1,6 +1,10 @@ +import atexit import logging import os +import signal +import sys from pathlib import Path +from typing import Dict, List, Type from core import utils from core.configservice.manager import ConfigServiceManager @@ -14,12 +18,31 @@ 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 + """ + logger.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: """ 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. @@ -30,13 +53,13 @@ class CoreEmu: # configuration config = config if config else {} - self.config: dict[str, str] = 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_errors: List[str] = [] self.service_manager: ConfigServiceManager = ConfigServiceManager() self._load_services() @@ -47,6 +70,9 @@ class CoreEmu: # 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. @@ -118,7 +144,7 @@ class CoreEmu: _, session = self.sessions.popitem() 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. diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 7d3dc8dc..de5b3559 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -2,7 +2,7 @@ CORE data objects. """ from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, 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 @@ -93,9 +93,9 @@ class NodeOptions: emane: str = None legacy: bool = False # src, dst - binds: list[tuple[str, str]] = field(default_factory=list) + binds: List[Tuple[str, str]] = field(default_factory=list) # src, dst, unique, delete - volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list) + volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) def set_position(self, x: float, y: float) -> None: """ @@ -148,7 +148,7 @@ class InterfaceData: 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. diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 1c0d3c92..1d09ce1e 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -8,7 +8,7 @@ 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 @@ -48,7 +48,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. @@ -105,7 +105,7 @@ class DistributedServer: """ 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)) os.unlink(temp.name) @@ -123,8 +123,8 @@ class DistributedController: :param session: session """ self.session: "Session" = session - self.servers: dict[str, DistributedServer] = OrderedDict() - self.tunnels: dict[int, tuple[GreTap, GreTap]] = {} + self.servers: Dict[str, DistributedServer] = OrderedDict() + self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {} self.address: str = self.session.options.get("distributed_address") def add_server(self, name: str, host: str) -> None: @@ -187,7 +187,8 @@ class DistributedController: :return: nothing """ mtu = self.session.options.get_int("mtu") - for node in self.session.nodes.values(): + for node_id in self.session.nodes: + node = self.session.nodes[node_id] if not isinstance(node, CtrlNet) or node.serverintf is not None: continue for name in self.servers: @@ -213,7 +214,7 @@ class DistributedController: def create_gre_tunnel( self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool - ) -> tuple[GreTap, GreTap]: + ) -> Tuple[GreTap, GreTap]: """ Create gre tunnel using a pair of gre taps between the local and remote server. diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 96fb919b..e04d382b 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -50,7 +50,6 @@ class NodeTypes(Enum): DOCKER = 15 LXC = 16 WIRELESS = 17 - PODMAN = 18 class LinkTypes(Enum): diff --git a/daemon/core/emulator/hooks.py b/daemon/core/emulator/hooks.py deleted file mode 100644 index ffeeafeb..00000000 --- a/daemon/core/emulator/hooks.py +++ /dev/null @@ -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}" - ) diff --git a/daemon/core/emulator/links.py b/daemon/core/emulator/links.py index 5df29d90..22f75b98 100644 --- a/daemon/core/emulator/links.py +++ b/daemon/core/emulator/links.py @@ -4,9 +4,8 @@ for a session. """ import logging -from collections.abc import ValuesView from dataclasses import dataclass -from typing import Optional +from typing import Dict, Optional, Tuple, ValuesView from core.emulator.data import LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags @@ -16,7 +15,7 @@ from core.nodes.interface import CoreInterface from core.nodes.network import PtpNet logger = logging.getLogger(__name__) -LinkKeyType = tuple[int, Optional[int], int, Optional[int]] +LinkKeyType = Tuple[int, Optional[int], int, Optional[int]] def create_key( @@ -146,8 +145,8 @@ class LinkManager: """ Create a LinkManager instance. """ - self._links: dict[LinkKeyType, CoreLink] = {} - self._node_links: dict[int, dict[LinkKeyType, CoreLink]] = {} + self._links: Dict[LinkKeyType, CoreLink] = {} + self._node_links: Dict[int, Dict[LinkKeyType, CoreLink]] = {} def add(self, core_link: CoreLink) -> None: """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 5a6557ee..7f8144d0 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -14,7 +14,7 @@ import tempfile import threading import time from pathlib import Path -from typing import Callable, Optional, TypeVar, Union +from typing import Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union from core import constants, utils from core.configservice.manager import ConfigServiceManager @@ -57,7 +57,6 @@ from core.nodes.network import ( WlanNode, ) from core.nodes.physical import PhysicalNode, Rj45Node -from core.nodes.podman import PodmanNode from core.nodes.wireless import WirelessNode from core.plugins.sdt import Sdt from core.services.coreservices import CoreServices @@ -67,7 +66,7 @@ from core.xml.corexml import CoreXmlReader, CoreXmlWriter logger = logging.getLogger(__name__) # maps for converting from API call node type values to classes and vice versa -NODES: dict[NodeTypes, type[NodeBase]] = { +NODES: Dict[NodeTypes, Type[NodeBase]] = { NodeTypes.DEFAULT: CoreNode, NodeTypes.PHYSICAL: PhysicalNode, NodeTypes.SWITCH: SwitchNode, @@ -82,13 +81,13 @@ NODES: dict[NodeTypes, type[NodeBase]] = { NodeTypes.DOCKER: DockerNode, NodeTypes.LXC: LxcNode, NodeTypes.WIRELESS: WirelessNode, - NodeTypes.PODMAN: PodmanNode, } -NODES_TYPE: dict[type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} +NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} +CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode} CTRL_NET_ID: int = 9001 -LINK_COLORS: list[str] = ["green", "blue", "orange", "purple", "turquoise"] +LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] NT: TypeVar = TypeVar("NT", bound=NodeBase) -WIRELESS_TYPE: tuple[type[WlanNode], type[EmaneNet], type[WirelessNode]] = ( +WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet], Type[WirelessNode]] = ( WlanNode, EmaneNet, WirelessNode, @@ -101,7 +100,7 @@ class Session: """ def __init__( - self, _id: int, config: dict[str, str] = None, mkdir: bool = True + self, _id: int, config: Dict[str, str] = None, mkdir: bool = True ) -> None: """ Create a Session instance. @@ -122,33 +121,33 @@ class Session: self.thumbnail: Optional[Path] = None self.user: Optional[str] = None self.event_loop: EventLoop = EventLoop() - self.link_colors: dict[int, str] = {} + self.link_colors: Dict[int, str] = {} # dict of nodes: all nodes and nets - self.nodes: dict[int, NodeBase] = {} + self.nodes: Dict[int, NodeBase] = {} self.nodes_lock: threading.Lock = threading.Lock() self.link_manager: LinkManager = LinkManager() # states and hooks handlers self.state: EventTypes = EventTypes.DEFINITION_STATE self.state_time: float = time.monotonic() - self.hooks: dict[EventTypes, list[tuple[str, str]]] = {} - self.state_hooks: dict[EventTypes, list[Callable[[EventTypes], None]]] = {} + self.hooks: Dict[EventTypes, List[Tuple[str, str]]] = {} + self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {} self.add_state_hook( state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook ) # handlers for broadcasting information - self.event_handlers: list[Callable[[EventData], None]] = [] - self.exception_handlers: list[Callable[[ExceptionData], None]] = [] - self.node_handlers: list[Callable[[NodeData], None]] = [] - self.link_handlers: list[Callable[[LinkData], None]] = [] - self.file_handlers: list[Callable[[FileData], None]] = [] - self.config_handlers: list[Callable[[ConfigData], None]] = [] + self.event_handlers: List[Callable[[EventData], None]] = [] + self.exception_handlers: List[Callable[[ExceptionData], None]] = [] + self.node_handlers: List[Callable[[NodeData], None]] = [] + self.link_handlers: List[Callable[[LinkData], None]] = [] + self.file_handlers: List[Callable[[FileData], None]] = [] + self.config_handlers: List[Callable[[ConfigData], None]] = [] # session options/metadata self.options: SessionConfig = SessionConfig(config) - self.metadata: dict[str, str] = {} + self.metadata: Dict[str, str] = {} # distributed support and logic self.distributed: DistributedController = DistributedController(self) @@ -164,7 +163,7 @@ class Session: self.service_manager: Optional[ConfigServiceManager] = None @classmethod - def get_node_class(cls, _type: NodeTypes) -> type[NodeBase]: + def get_node_class(cls, _type: NodeTypes) -> Type[NodeBase]: """ Retrieve the class for a given node type. @@ -177,7 +176,7 @@ class Session: return node_class @classmethod - def get_node_type(cls, _class: type[NodeBase]) -> NodeTypes: + def get_node_type(cls, _class: Type[NodeBase]) -> NodeTypes: """ Retrieve node type for a given node class. @@ -239,7 +238,7 @@ class Session: iface1_data: InterfaceData = None, iface2_data: InterfaceData = None, options: LinkOptions = None, - ) -> tuple[Optional[CoreInterface], Optional[CoreInterface]]: + ) -> Tuple[Optional[CoreInterface], Optional[CoreInterface]]: """ Add a link between nodes. @@ -346,7 +345,7 @@ class Session: iface1_data: InterfaceData = None, iface2_data: InterfaceData = None, options: LinkOptions = None, - ) -> tuple[CoreInterface, CoreInterface]: + ) -> Tuple[CoreInterface, CoreInterface]: """ Create a wired link between two nodes. @@ -477,7 +476,7 @@ class Session: def add_node( self, - _class: type[NT], + _class: Type[NT], _id: int = None, name: str = None, server: str = None, @@ -519,9 +518,11 @@ class Session: self.set_node_pos(node, position.x, position.y) # setup default wlan if isinstance(node, WlanNode): - self.mobility.set_model_config(node.id, BasicRangeModel.name) + self.mobility.set_model_config(self.id, BasicRangeModel.name) # boot core nodes after runtime - if self.is_running() and isinstance(node, CoreNode): + is_runtime = self.state == EventTypes.RUNTIME_STATE + if is_runtime and isinstance(node, CoreNode): + self.write_nodes() self.add_remove_control_iface(node, remove=False) self.boot_node(node) self.sdt.add_node(node) @@ -728,12 +729,27 @@ class Session: self.state = state self.state_time = time.monotonic() logger.info("changing session(%s) to state %s", self.id, state.name) + self.write_state(state) self.run_hooks(state) self.run_state_hooks(state) if send_event: event_data = EventData(event_type=state, time=str(time.monotonic())) self.broadcast_event(event_data) + def write_state(self, state: EventTypes) -> None: + """ + Write the state to a state file in the session dir. + + :param state: state to write to file + :return: nothing + """ + state_file = self.directory / "state" + try: + with state_file.open("w") as f: + f.write(f"{state.value} {state.name}\n") + except IOError: + logger.exception("error writing state file: %s", state.name) + def run_hooks(self, state: EventTypes) -> None: """ Run hook scripts upon changing states. If hooks is not specified, run all hooks @@ -746,7 +762,7 @@ class Session: for hook in hooks: self.run_hook(hook) - def run_hook(self, hook: tuple[str, str]) -> None: + def run_hook(self, hook: Tuple[str, str]) -> None: """ Run a hook. @@ -770,7 +786,7 @@ class Session: cwd=self.directory, env=self.get_environment(), ) - except (OSError, subprocess.CalledProcessError): + except (IOError, subprocess.CalledProcessError): logger.exception("error running hook: %s", file_path) def run_state_hooks(self, state: EventTypes) -> None: @@ -836,7 +852,7 @@ class Session: xml_file_path = self.directory / "session-deployed.xml" xml_writer.write(xml_file_path) - def get_environment(self, state: bool = True) -> dict[str, str]: + def get_environment(self, state: bool = True) -> Dict[str, str]: """ Get an environment suitable for a subprocess.Popen call. This is the current process environment with some session-specific @@ -871,7 +887,7 @@ class Session: if path.is_file(): try: utils.load_config(path, env) - except OSError: + except IOError: logger.exception("error reading environment file: %s", path) return env @@ -888,12 +904,12 @@ class Session: uid = pwd.getpwnam(user).pw_uid gid = self.directory.stat().st_gid os.chown(self.directory, uid, gid) - except OSError: + except IOError: logger.exception("failed to set permission on %s", self.directory) def create_node( self, - _class: type[NT], + _class: Type[NT], start: bool, _id: int = None, name: str = None, @@ -929,7 +945,7 @@ class Session: node.startup() return node - def get_node(self, _id: int, _class: type[NT]) -> NT: + def get_node(self, _id: int, _class: Type[NT]) -> NT: """ Get a session node. @@ -981,6 +997,21 @@ class Session: for node_id in nodes_ids: self.sdt.delete_node(node_id) + def write_nodes(self) -> None: + """ + Write nodes to a 'nodes' file in the session dir. + The 'nodes' file lists: number, name, api-type, class-type + """ + file_path = self.directory / "nodes" + try: + with self.nodes_lock: + with file_path.open("w") as f: + for _id, node in self.nodes.items(): + node_type = self.get_node_type(type(node)) + f.write(f"{_id} {node.name} {node_type} {type(node)}\n") + except IOError: + logger.exception("error writing nodes file") + def exception( self, level: ExceptionLevels, source: str, text: str, node_id: int = None ) -> None: @@ -1003,7 +1034,7 @@ class Session: ) self.broadcast_exception(exception_data) - def instantiate(self) -> list[Exception]: + def instantiate(self) -> List[Exception]: """ We have entered the instantiation state, invoke startup methods of various managers and boot the nodes. Validate nodes and check @@ -1011,9 +1042,11 @@ class Session: :return: list of service boot errors during startup """ - if self.is_running(): + if self.state == EventTypes.RUNTIME_STATE: logger.warning("ignoring instantiate, already in runtime state") return [] + # write current nodes out to session directory file + self.write_nodes() # create control net interfaces and network tunnels # which need to exist for emane to sync on location events # in distributed scenarios @@ -1083,7 +1116,6 @@ class Session: if isinstance(node, CoreNodeBase) and node.up: args = (node,) funcs.append((self.services.stop_services, args, {})) - funcs.append((node.stop_config_services, (), {})) utils.threadpool(funcs) # shutdown emane @@ -1114,16 +1146,11 @@ class Session: :param node: node to boot :return: nothing """ - logger.info( - "booting node(%s): config services(%s) services(%s)", - node.name, - ", ".join(node.config_services.keys()), - ", ".join(x.name for x in node.services), - ) + logger.info("booting node(%s): %s", node.name, [x.name for x in node.services]) self.services.boot_services(node) node.start_config_services() - def boot_nodes(self) -> list[Exception]: + def boot_nodes(self) -> List[Exception]: """ Invoke the boot() procedure for all nodes and send back node messages to the GUI for node messages that had the status @@ -1145,7 +1172,7 @@ class Session: self.update_control_iface_hosts() return exceptions - def get_control_net_prefixes(self) -> list[str]: + def get_control_net_prefixes(self) -> List[str]: """ Retrieve control net prefixes. @@ -1160,7 +1187,7 @@ class Session: p0 = p return [p0, p1, p2, p3] - def get_control_net_server_ifaces(self) -> list[str]: + def get_control_net_server_ifaces(self) -> List[str]: """ Retrieve control net server interfaces. @@ -1366,7 +1393,7 @@ class Session: Return the current time we have been in the runtime state, or zero if not in runtime. """ - if self.is_running(): + if self.state == EventTypes.RUNTIME_STATE: return time.monotonic() - self.state_time else: return 0.0 @@ -1443,11 +1470,3 @@ class Session: color = LINK_COLORS[index] self.link_colors[network_id] = color return color - - def is_running(self) -> bool: - """ - Convenience for checking if this session is in the runtime state. - - :return: True if in the runtime state, False otherwise - """ - return self.state == EventTypes.RUNTIME_STATE diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index b6d5bcd3..ead9e9e5 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, List, Optional from core.config import ConfigBool, ConfigInt, ConfigString, Configuration from core.errors import CoreError @@ -10,7 +10,7 @@ class SessionConfig: Provides session configuration. """ - options: list[Configuration] = [ + options: List[Configuration] = [ ConfigString(id="controlnet", label="Control Network"), ConfigString(id="controlnet0", label="Control Network 0"), ConfigString(id="controlnet1", label="Control Network 1"), @@ -35,16 +35,16 @@ class SessionConfig: ConfigInt(id="mtu", default="0", label="MTU for All Devices"), ] - def __init__(self, config: dict[str, str] = None) -> None: + def __init__(self, config: Dict[str, str] = None) -> None: """ Create a SessionConfig instance. :param config: configuration to initialize with """ - self._config: dict[str, str] = {x.id: x.default for x in self.options} + self._config: Dict[str, str] = {x.id: x.default for x in self.options} self._config.update(config or {}) - def update(self, config: dict[str, str]) -> None: + def update(self, config: Dict[str, str]) -> None: """ Update current configuration with provided values. @@ -73,7 +73,7 @@ class SessionConfig: """ return self._config.get(name, default) - def all(self) -> dict[str, str]: + def all(self) -> Dict[str, str]: """ Retrieve all configuration options. diff --git a/daemon/core/executables.py b/daemon/core/executables.py index f04d88de..95c97378 100644 --- a/daemon/core/executables.py +++ b/daemon/core/executables.py @@ -1,3 +1,5 @@ +from typing import List + BASH: str = "bash" ETHTOOL: str = "ethtool" IP: str = "ip" @@ -11,7 +13,7 @@ UMOUNT: str = "umount" VCMD: str = "vcmd" VNODED: str = "vnoded" -COMMON_REQUIREMENTS: list[str] = [ +COMMON_REQUIREMENTS: List[str] = [ BASH, ETHTOOL, IP, @@ -24,10 +26,10 @@ COMMON_REQUIREMENTS: list[str] = [ VCMD, VNODED, ] -OVS_REQUIREMENTS: list[str] = [OVS_VSCTL] +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. diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 4fd1dce5..d905bff3 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -3,7 +3,7 @@ import math import tkinter as tk from tkinter import PhotoImage, font, messagebox, ttk from tkinter.ttk import Progressbar -from typing import Any, Optional +from typing import Any, Dict, Optional, Type import grpc @@ -45,7 +45,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 @@ -145,7 +145,7 @@ class Application(ttk.Frame): self.statusbar = StatusBar(self.right_frame, self) self.statusbar.grid(sticky=tk.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() diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 0a5ae76b..04f2fdcb 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -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,17 +118,17 @@ 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( + 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.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) @@ -136,16 +136,16 @@ class IpConfigs(yaml.YAMLObject): 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 +158,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() diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index da2ca6d6..aab9cdb9 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,10 +6,9 @@ import json import logging import os import tkinter as tk -from collections.abc import Iterable from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc @@ -17,7 +16,6 @@ from core.api.grpc import client, configservices_pb2, core_pb2 from core.api.grpc.wrappers import ( ConfigOption, ConfigService, - ConfigServiceDefaults, EmaneModelConfig, Event, ExceptionEvent, @@ -57,7 +55,7 @@ GUI_SOURCE = "gui" CPU_USAGE_DELAY = 3 -def to_dict(config: dict[str, ConfigOption]) -> dict[str, str]: +def to_dict(config: Dict[str, ConfigOption]) -> Dict[str, str]: return {x: y.value for x, y in config.items()} @@ -72,30 +70,27 @@ class CoreClient: self.session: Optional[Session] = None self.user = getpass.getuser() - # menu options - self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False) - # global service settings - self.services: dict[str, set[str]] = {} - self.config_services_groups: dict[str, set[str]] = {} - self.config_services: dict[str, ConfigService] = {} + self.services: Dict[str, Set[str]] = {} + self.config_services_groups: Dict[str, Set[str]] = {} + self.config_services: Dict[str, ConfigService] = {} # loaded configuration data - self.emane_models: list[str] = [] - self.servers: dict[str, CoreServer] = {} - self.custom_nodes: dict[str, NodeDraw] = {} - self.custom_observers: dict[str, Observer] = {} + self.emane_models: List[str] = [] + self.servers: Dict[str, CoreServer] = {} + self.custom_nodes: Dict[str, NodeDraw] = {} + self.custom_observers: Dict[str, Observer] = {} self.read_config() # helpers - self.iface_to_edge: dict[tuple[int, ...], CanvasEdge] = {} + self.iface_to_edge: Dict[Tuple[int, ...], CanvasEdge] = {} self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) self.observer: Optional[str] = None # session data - self.mobility_players: dict[int, MobilityPlayer] = {} - self.canvas_nodes: dict[int, CanvasNode] = {} - self.links: dict[str, CanvasEdge] = {} + self.mobility_players: Dict[int, MobilityPlayer] = {} + self.canvas_nodes: Dict[int, CanvasNode] = {} + self.links: Dict[str, CanvasEdge] = {} self.handling_throughputs: Optional[grpc.Future] = None self.handling_cpu_usage: Optional[grpc.Future] = None self.handling_events: Optional[grpc.Future] = None @@ -247,10 +242,9 @@ class CoreClient: logger.warning("unknown node event: %s", event) def enable_throughputs(self) -> None: - if not self.handling_throughputs: - self.handling_throughputs = self.client.throughputs( - self.session.id, self.handle_throughputs - ) + self.handling_throughputs = self.client.throughputs( + self.session.id, self.handle_throughputs + ) def cancel_throughputs(self) -> None: if self.handling_throughputs: @@ -374,7 +368,7 @@ class CoreClient: # existing session sessions = self.client.get_sessions() if session_id: - session_ids = {x.id for x in sessions} + session_ids = set(x.id for x in sessions) if session_id not in session_ids: self.app.show_error( "Join Session Error", @@ -403,7 +397,7 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) - def get_links(self, definition: bool = False) -> list[Link]: + def get_links(self, definition: bool = False) -> List[Link]: if not definition: self.ifaces_manager.set_macs([x.link for x in self.links.values()]) links = [] @@ -421,7 +415,7 @@ class CoreClient: links.append(edge.asymmetric_link) return links - def start_session(self, definition: bool = False) -> tuple[bool, list[str]]: + def start_session(self, definition: bool = False) -> Tuple[bool, List[str]]: self.session.links = self.get_links(definition) self.session.metadata = self.get_metadata() self.session.servers.clear() @@ -437,15 +431,13 @@ class CoreClient: definition, result, ) - if self.show_throughputs.get(): - self.enable_throughputs() except grpc.RpcError as e: self.app.show_grpc_exception("Start Session Error", e) return result, exceptions def stop_session(self, session_id: int = None) -> bool: - session_id = session_id or self.session.id - self.cancel_throughputs() + if not session_id: + session_id = self.session.id result = False try: result = self.client.stop_session(session_id) @@ -463,7 +455,7 @@ class CoreClient: self.mobility_players[node.id] = mobility_player mobility_player.show() - def get_metadata(self) -> dict[str, str]: + def get_metadata(self) -> Dict[str, str]: # create canvas data canvas_config = self.app.manager.get_metadata() canvas_config = json.dumps(canvas_config) @@ -654,7 +646,7 @@ class CoreClient: self.session.nodes[node.id] = node return node - def deleted_canvas_nodes(self, canvas_nodes: list[CanvasNode]) -> None: + def deleted_canvas_nodes(self, canvas_nodes: List[CanvasNode]) -> None: """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces @@ -675,14 +667,14 @@ class CoreClient: self.links[edge.token] = edge src_node = edge.src.core_node dst_node = edge.dst.core_node - if edge.link.iface1: + if nutils.is_container(src_node): src_iface_id = edge.link.iface1.id self.iface_to_edge[(src_node.id, src_iface_id)] = edge - if edge.link.iface2: + if nutils.is_container(dst_node): dst_iface_id = edge.link.iface2.id self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge - def get_wlan_configs(self) -> list[tuple[int, dict[str, str]]]: + def get_wlan_configs(self) -> List[Tuple[int, Dict[str, str]]]: configs = [] for node in self.session.nodes.values(): if node.type != NodeType.WIRELESS_LAN: @@ -693,7 +685,7 @@ class CoreClient: configs.append((node.id, config)) return configs - def get_mobility_configs(self) -> list[tuple[int, dict[str, str]]]: + def get_mobility_configs(self) -> List[Tuple[int, Dict[str, str]]]: configs = [] for node in self.session.nodes.values(): if not nutils.is_mobility(node): @@ -704,7 +696,7 @@ class CoreClient: configs.append((node.id, config)) return configs - def get_emane_model_configs(self) -> list[EmaneModelConfig]: + def get_emane_model_configs(self) -> List[EmaneModelConfig]: configs = [] for node in self.session.nodes.values(): for key, config in node.emane_model_configs.items(): @@ -718,7 +710,7 @@ class CoreClient: configs.append(config) return configs - def get_service_configs(self) -> list[ServiceConfig]: + def get_service_configs(self) -> List[ServiceConfig]: configs = [] for node in self.session.nodes.values(): if not nutils.is_container(node): @@ -738,7 +730,7 @@ class CoreClient: configs.append(config) return configs - def get_service_file_configs(self) -> list[ServiceFileConfig]: + def get_service_file_configs(self) -> List[ServiceFileConfig]: configs = [] for node in self.session.nodes.values(): if not nutils.is_container(node): @@ -751,17 +743,12 @@ class CoreClient: configs.append(config) return configs - def get_config_service_rendered(self, node_id: int, name: str) -> dict[str, str]: + def get_config_service_rendered(self, node_id: int, name: str) -> Dict[str, str]: return self.client.get_config_service_rendered(self.session.id, node_id, name) - def get_config_service_defaults( - self, node_id: int, name: str - ) -> ConfigServiceDefaults: - return self.client.get_config_service_defaults(self.session.id, node_id, name) - def get_config_service_configs_proto( - self, - ) -> list[configservices_pb2.ConfigServiceConfig]: + self + ) -> List[configservices_pb2.ConfigServiceConfig]: config_service_protos = [] for node in self.session.nodes.values(): if not nutils.is_container(node): @@ -783,7 +770,7 @@ class CoreClient: _, output = self.client.node_command(self.session.id, node_id, self.observer) return output - def get_wlan_config(self, node_id: int) -> dict[str, ConfigOption]: + def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: config = self.client.get_wlan_config(self.session.id, node_id) logger.debug( "get wlan configuration from node %s, result configuration: %s", @@ -792,10 +779,10 @@ class CoreClient: ) return config - def get_wireless_config(self, node_id: int) -> dict[str, ConfigOption]: + def get_wireless_config(self, node_id: int) -> Dict[str, ConfigOption]: return self.client.get_wireless_config(self.session.id, node_id) - def get_mobility_config(self, node_id: int) -> dict[str, ConfigOption]: + def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: config = self.client.get_mobility_config(self.session.id, node_id) logger.debug( "get mobility config from node %s, result configuration: %s", @@ -806,7 +793,7 @@ class CoreClient: def get_emane_model_config( self, node_id: int, model: str, iface_id: int = None - ) -> dict[str, ConfigOption]: + ) -> Dict[str, ConfigOption]: if iface_id is None: iface_id = -1 config = self.client.get_emane_model_config( diff --git a/daemon/core/gui/data/icons/podman.png b/daemon/core/gui/data/icons/podman.png deleted file mode 100644 index 771e04a0..00000000 Binary files a/daemon/core/gui/data/icons/podman.png and /dev/null differ diff --git a/daemon/core/gui/data/xmls/emane-demo-antenna.xml b/daemon/core/gui/data/xmls/emane-demo-antenna.xml index 00616339..935fd97a 100644 --- a/daemon/core/gui/data/xmls/emane-demo-antenna.xml +++ b/daemon/core/gui/data/xmls/emane-demo-antenna.xml @@ -1,373 +1,88 @@ - + - - + + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -382,17 +97,6 @@ - - - - - - - - - - - @@ -409,10 +113,179 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -425,7 +298,7 @@ - + @@ -438,19 +311,10 @@ - - - - - - - - - - - + + diff --git a/daemon/core/gui/data/xmls/emane-demo-eel.xml b/daemon/core/gui/data/xmls/emane-demo-eel.xml index 66f8f2a8..4162458c 100644 --- a/daemon/core/gui/data/xmls/emane-demo-eel.xml +++ b/daemon/core/gui/data/xmls/emane-demo-eel.xml @@ -1,55 +1,66 @@ - + - - + + - - + + - + - - + + - + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - @@ -64,17 +75,6 @@ - - - - - - - - - - - @@ -91,10 +91,7 @@ - - - @@ -107,7 +104,7 @@ - + @@ -120,19 +117,10 @@ - - - - - - - - - - - + + diff --git a/daemon/core/gui/data/xmls/emane-demo-files.xml b/daemon/core/gui/data/xmls/emane-demo-files.xml index 9e71d58f..da6f9c70 100644 --- a/daemon/core/gui/data/xmls/emane-demo-files.xml +++ b/daemon/core/gui/data/xmls/emane-demo-files.xml @@ -1,55 +1,66 @@ - + - - + + - - + + - + - - + + - + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - @@ -64,17 +75,6 @@ - - - - - - - - - - - @@ -91,10 +91,7 @@ - - - @@ -107,7 +104,7 @@ - + @@ -120,19 +117,10 @@ - - - - - - - - - - - + + diff --git a/daemon/core/gui/data/xmls/emane-demo-gpsd.xml b/daemon/core/gui/data/xmls/emane-demo-gpsd.xml index 2dbc1294..06bc54dc 100644 --- a/daemon/core/gui/data/xmls/emane-demo-gpsd.xml +++ b/daemon/core/gui/data/xmls/emane-demo-gpsd.xml @@ -1,55 +1,66 @@ - + - - + + - - + + - + - - + + - + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - @@ -64,17 +75,6 @@ - - - - - - - - - - - @@ -91,10 +91,7 @@ - - - @@ -107,7 +104,7 @@ - + @@ -120,19 +117,10 @@ - - - - - - - - - - - + + diff --git a/daemon/core/gui/data/xmls/emane-demo-precomputed.xml b/daemon/core/gui/data/xmls/emane-demo-precomputed.xml index d53e26ba..a19acba6 100644 --- a/daemon/core/gui/data/xmls/emane-demo-precomputed.xml +++ b/daemon/core/gui/data/xmls/emane-demo-precomputed.xml @@ -1,55 +1,66 @@ - + - - + + - - + + - + - - + + - + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - @@ -64,17 +75,6 @@ - - - - - - - - - - - @@ -91,10 +91,7 @@ - - - @@ -107,7 +104,7 @@ - + @@ -120,19 +117,10 @@ - - - - - - - - - - - + + diff --git a/daemon/core/gui/data/xmls/sample1.xml b/daemon/core/gui/data/xmls/sample1.xml index 64d093a8..c4f75c47 100644 --- a/daemon/core/gui/data/xmls/sample1.xml +++ b/daemon/core/gui/data/xmls/sample1.xml @@ -95,9 +95,10 @@ - + + @@ -259,5 +260,9 @@ + + + + diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index b13f0797..9e430214 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -3,7 +3,7 @@ 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.gui.dialogs.dialog import Dialog @@ -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: diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 5b0f27b3..0ef294c7 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -4,7 +4,7 @@ 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 @@ -32,7 +32,7 @@ class CanvasWallpaperDialog(Dialog): ) 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: diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index a27b1698..a2f131d4 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -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 (#). - - :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 (#) 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) @@ -96,7 +66,7 @@ class ColorPickerDialog(Dialog): ) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.red_label = ttk.Label( - frame, background=get_rgb(self.red.get(), 0, 0), width=5 + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 ) self.red_label.grid(row=0, column=3, sticky=tk.EW) @@ -119,7 +89,7 @@ class ColorPickerDialog(Dialog): ) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.green_label = ttk.Label( - frame, background=get_rgb(0, self.green.get(), 0), width=5 + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 ) self.green_label.grid(row=0, column=3, sticky=tk.EW) @@ -142,7 +112,7 @@ class ColorPickerDialog(Dialog): ) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.blue_label = ttk.Label( - frame, background=get_rgb(0, 0, self.blue.get()), width=5 + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 ) self.blue_label.grid(row=0, column=3, sticky=tk.EW) @@ -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) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 0e873a79..62c0bfc5 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -4,7 +4,7 @@ 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 @@ -35,22 +35,22 @@ class ConfigServiceConfigDialog(Dialog): self.node: Node = node 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.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.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.modes: List[str] = [] + self.mode_configs: Dict[str, Dict[str, str]] = {} self.notebook: Optional[ttk.Notebook] = None self.templates_combobox: Optional[ttk.Combobox] = None self.modes_combobox: Optional[ttk.Combobox] = None @@ -62,12 +62,12 @@ class ConfigServiceConfigDialog(Dialog): 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: @@ -87,9 +87,7 @@ 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 - ) + defaults = self.core.client.get_config_service_defaults(self.service_name) self.original_service_files = defaults.templates self.temp_service_files = dict(self.original_service_files) self.modes = sorted(defaults.modes) @@ -407,7 +405,7 @@ class ConfigServiceConfigDialog(Dialog): 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) diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index 6b2f4927..b205e175 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -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: diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index ea4421e8..065cc43e 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -2,7 +2,7 @@ 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 @@ -21,13 +21,13 @@ if TYPE_CHECKING: 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: @@ -45,7 +45,7 @@ class ServicesSelectDialog(Dialog): 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): + for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) @@ -86,7 +86,7 @@ class ServicesSelectDialog(Dialog): 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 +114,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() @@ -147,7 +147,7 @@ class CustomNodesDialog(Dialog): 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 = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky=tk.EW) def draw_node_buttons(self) -> None: diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 00eda694..b3f6d9ce 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -4,7 +4,7 @@ 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 @@ -37,13 +37,11 @@ class EmaneModelDialog(Dialog): 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)) 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) @@ -82,7 +80,7 @@ class EmaneConfigDialog(Dialog): self.node: Node = 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] diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index 391df18f..474dc2d0 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import messagebox, ttk +from tkinter import ttk from typing import TYPE_CHECKING, Optional from core.api.grpc.wrappers import Hook, SessionState @@ -91,13 +91,6 @@ class HookDialog(Dialog): self.hook.file = file_name self.hook.data = data else: - if file_name in self.app.core.session.hooks: - messagebox.showerror( - "Hook Error", - f"Hook {file_name} already exists!", - parent=self.master, - ) - return self.hook = Hook(state=state, file=file_name, data=data) self.destroy() diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 99388548..68b5ab36 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional import netaddr @@ -17,8 +17,8 @@ class IpConfigDialog(Dialog): super().__init__(app, "IP Configuration") self.ip4: str = self.app.guiconfig.ips.ip4 self.ip6: str = self.app.guiconfig.ips.ip6 - self.ip4s: list[str] = self.app.guiconfig.ips.ip4s - self.ip6s: list[str] = self.app.guiconfig.ips.ip6s + self.ip4s: List[str] = self.app.guiconfig.ips.ip4s + self.ip6s: List[str] = self.app.guiconfig.ips.ip6s self.ip4_entry: Optional[ttk.Entry] = None self.ip4_listbox: Optional[ListboxScroll] = None self.ip6_entry: Optional[ttk.Entry] = None diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 6a2991aa..b22c5fef 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -3,7 +3,7 @@ mobility configuration """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional import grpc @@ -26,7 +26,7 @@ class MobilityConfigDialog(Dialog): config = self.node.mobility_config if not config: config = self.app.core.get_mobility_config(self.node.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 Mobility Config Error", e) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 162696d4..c9ca67f5 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import logging import tkinter as tk from functools import partial from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional import netaddr from PIL.ImageTk import PhotoImage @@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog): if self.node.server: server = self.node.server self.server: tk.StringVar = tk.StringVar(value=server) - self.ifaces: dict[int, InterfaceData] = {} + self.ifaces: Dict[int, InterfaceData] = {} self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index ce718080..cddbdb03 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -4,7 +4,7 @@ core node services import logging import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Set from core.api.grpc.wrappers import Node from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog @@ -20,7 +20,7 @@ if TYPE_CHECKING: class NodeConfigServiceDialog(Dialog): def __init__( - self, app: "Application", node: Node, services: set[str] = None + self, app: "Application", node: Node, services: Set[str] = None ) -> None: title = f"{node.name} Config Services" super().__init__(app, title) @@ -30,7 +30,7 @@ class NodeConfigServiceDialog(Dialog): self.current: Optional[ListboxScroll] = None if services is None: services = set(node.config_services) - self.current_services: set[str] = services + self.current_services: Set[str] = services self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.draw() diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 66e83fa4..431d5c3d 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -3,7 +3,7 @@ core node services """ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Set from core.api.grpc.wrappers import Node from core.gui.dialogs.dialog import Dialog @@ -24,7 +24,7 @@ class NodeServiceDialog(Dialog): self.services: Optional[CheckboxList] = None self.current: Optional[ListboxScroll] = None services = set(node.services) - self.current_services: set[str] = services + self.current_services: Set[str] = services self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.draw() diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 75789893..494020e3 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional from core.gui import nodeutils as nutils from core.gui.dialogs.dialog import Dialog @@ -17,7 +17,7 @@ class RunToolDialog(Dialog): self.cmd: tk.StringVar = tk.StringVar(value="ps ax") self.result: Optional[CodeText] = None self.node_list: Optional[ListboxScroll] = None - self.executable_nodes: dict[str, int] = {} + self.executable_nodes: Dict[str, int] = {} self.store_nodes() self.draw() diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 5eec7faf..6f6b0d24 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -2,7 +2,7 @@ import logging import tkinter as tk from pathlib import Path from tkinter import filedialog, messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import grpc from PIL.ImageTk import PhotoImage @@ -35,21 +35,21 @@ class ServiceConfigDialog(Dialog): self.service_name: str = service_name self.radiovar: tk.IntVar = tk.IntVar(value=2) self.metadata: str = "" - self.filenames: 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.filenames: 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: Optional[float] = None self.directory_entry: Optional[ttk.Entry] = None - self.default_directories: list[str] = [] - self.temp_directories: list[str] = [] + self.default_directories: List[str] = [] + self.temp_directories: List[str] = [] self.documentnew_img: PhotoImage = self.app.get_enum_icon( ImageEnum.DOCUMENTNEW, width=ICON_SIZE ) @@ -67,10 +67,10 @@ class ServiceConfigDialog(Dialog): self.validation_mode_entry: Optional[ttk.Entry] = None self.service_file_data: Optional[CodeText] = None self.validation_period_entry: Optional[ttk.Entry] = None - self.original_service_files: dict[str, str] = {} + self.original_service_files: Dict[str, str] = {} self.default_config: Optional[NodeServiceData] = None - self.temp_service_files: dict[str, str] = {} - self.modified_files: set[str] = set() + self.temp_service_files: Dict[str, str] = {} + self.modified_files: Set[str] = set() self.has_error: bool = False self.load() if not self.has_error: @@ -558,13 +558,13 @@ class ServiceConfigDialog(Dialog): @classmethod def append_commands( - cls, commands: list[str], listbox: tk.Listbox, to_add: list[str] + cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] ) -> None: for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) - def get_commands(self) -> tuple[list[str], list[str], list[str]]: + def get_commands(self) -> Tuple[List[str], List[str], List[str]]: startup = self.startup_commands_listbox.get(0, "end") shutdown = self.shutdown_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end") diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 3ca4fa63..deca7404 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -1,7 +1,7 @@ import logging import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional import grpc @@ -30,7 +30,7 @@ class SessionsDialog(Dialog): self.protocol("WM_DELETE_WINDOW", self.on_closing) self.draw() - def get_sessions(self) -> list[SessionSummary]: + def get_sessions(self) -> List[SessionSummary]: try: sessions = self.app.core.client.get_sessions() logger.info("sessions: %s", sessions) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index db19ff1a..d0e200ee 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -3,7 +3,7 @@ shape input dialog """ import tkinter as tk from tkinter import font, ttk -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, List, Optional, Union from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog @@ -16,8 +16,8 @@ if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph from core.gui.graph.shape import Shape -FONT_SIZES: list[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] -BORDER_WIDTH: list[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] +BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): @@ -168,7 +168,7 @@ class ShapeDialog(Dialog): self.add_text() self.destroy() - def make_font(self) -> list[Union[int, str]]: + def make_font(self) -> List[Union[int, str]]: """ create font for text or shape label """ diff --git a/daemon/core/gui/dialogs/wirelessconfig.py b/daemon/core/gui/dialogs/wirelessconfig.py index b04fbd2c..97e37b5f 100644 --- a/daemon/core/gui/dialogs/wirelessconfig.py +++ b/daemon/core/gui/dialogs/wirelessconfig.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional import grpc @@ -19,12 +19,12 @@ class WirelessConfigDialog(Dialog): super().__init__(app, f"Wireless Configuration - {canvas_node.core_node.name}") self.node: Node = canvas_node.core_node self.config_frame: Optional[ConfigFrame] = None - self.config: dict[str, ConfigOption] = {} + self.config: Dict[str, ConfigOption] = {} try: config = self.node.wireless_config if not config: config = self.app.core.get_wireless_config(self.node.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("Wireless Config Error", e) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index c382d3c8..237ca8a5 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional import grpc @@ -27,13 +27,13 @@ class WlanConfigDialog(Dialog): self.config_frame: Optional[ConfigFrame] = None self.range_entry: Optional[ttk.Entry] = None self.has_error: bool = False - self.ranges: dict[int, int] = {} + self.ranges: Dict[int, int] = {} self.positive_int: int = self.app.master.register(self.validate_and_update) try: config = self.node.wlan_config if not config: config = self.app.core.get_wlan_config(self.node.id) - self.config: dict[str, ConfigOption] = config + self.config: Dict[str, ConfigOption] = config self.init_draw_range() self.draw() except grpc.RpcError as e: diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index e5a4c97b..82fd0b97 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -2,10 +2,10 @@ import functools import logging import math import tkinter as tk -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Tuple, Union from core.api.grpc.wrappers import Interface, Link -from core.gui import nodeutils, themes +from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags @@ -54,9 +54,9 @@ def create_edge_token(link: Link) -> str: def node_label_positions( src_x: int, src_y: int, dst_x: int, dst_y: int -) -> tuple[tuple[float, float], tuple[float, float]]: +) -> Tuple[Tuple[float, float], Tuple[float, float]]: v_x, v_y = dst_x - src_x, dst_y - src_y - v_len = math.sqrt(v_x**2 + v_y**2) + v_len = math.sqrt(v_x ** 2 + v_y ** 2) if v_len == 0: u_x, u_y = 0.0, 0.0 else: @@ -128,8 +128,8 @@ class Edge: return self.width * self.app.app_scale def _get_arcpoint( - self, src_pos: tuple[float, float], dst_pos: tuple[float, float] - ) -> tuple[float, float]: + self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] + ) -> Tuple[float, float]: src_x, src_y = src_pos dst_x, dst_y = dst_pos mp_x = (src_x + dst_x) / 2 @@ -147,7 +147,7 @@ class Edge: perp_m = -1 / m b = mp_y - (perp_m * mp_x) # get arc x and y - offset = math.sqrt(self.arc**2 / (1 + (1 / m**2))) + offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2))) arc_x = mp_x if self.arc >= 0: arc_x += offset @@ -317,7 +317,7 @@ class Edge: if self.dst_label2: self.dst.canvas.itemconfig(self.dst_label2, text=text) - def drawing(self, pos: tuple[float, float]) -> None: + def drawing(self, pos: Tuple[float, float]) -> None: src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id) src_pos = src_x, src_y self.moved(src_pos, pos) @@ -368,7 +368,7 @@ class Edge: dst_pos = dst_x, dst_y self.moved(self.src.position(), dst_pos) - def moved(self, src_pos: tuple[float, float], dst_pos: tuple[float, float]) -> None: + def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: arc_pos = self._get_arcpoint(src_pos, dst_pos) self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) if self.middle_label: @@ -381,7 +381,7 @@ class Edge: self.src.canvas.coords(self.dst_label, *dst_pos) def moved2( - self, src_pos: tuple[float, float], dst_pos: tuple[float, float] + self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] ) -> None: arc_pos = self._get_arcpoint(src_pos, dst_pos) self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos) @@ -568,7 +568,7 @@ class CanvasEdge(Edge): label += f"{iface.ip6}/{iface.ip6_mask}" return label - def create_node_labels(self) -> tuple[str, str]: + def create_node_labels(self) -> Tuple[str, str]: label1 = None if self.link.iface1: label1 = self.iface_label(self.link.iface1) @@ -638,10 +638,10 @@ class CanvasEdge(Edge): self.check_wireless() if link is None: link = self.app.core.ifaces_manager.create_link(self) - if link.iface1 and not nodeutils.is_rj45(self.src.core_node): + if link.iface1: iface1 = link.iface1 self.src.ifaces[iface1.id] = iface1 - if link.iface2 and not nodeutils.is_rj45(self.dst.core_node): + if link.iface2: iface2 = link.iface2 self.dst.ifaces[iface2.id] = iface2 self.token = create_edge_token(link) @@ -751,9 +751,9 @@ class CanvasEdge(Edge): self.src.edges.discard(self) if self.dst: self.dst.edges.discard(self) - if self.link.iface1 and not nodeutils.is_rj45(self.src.core_node): + if self.link.iface1: del self.src.ifaces[self.link.iface1.id] - if self.link.iface2 and not nodeutils.is_rj45(self.dst.core_node): + if self.link.iface2: del self.dst.ifaces[self.link.iface2.id] if self.src.is_wireless(): self.dst.delete_antenna() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 1a701239..e3225a4d 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -2,7 +2,7 @@ import logging import tkinter as tk from copy import deepcopy from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage @@ -27,8 +27,8 @@ if TYPE_CHECKING: ZOOM_IN: float = 1.1 ZOOM_OUT: float = 0.9 -MOVE_NODE_MODES: set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT} -MOVE_SHAPE_MODES: set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT} +MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT} +MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT} BACKGROUND_COLOR: str = "#cccccc" @@ -40,32 +40,32 @@ class CanvasGraph(tk.Canvas): manager: "CanvasManager", core: "CoreClient", _id: int, - dimensions: tuple[int, int], + dimensions: Tuple[int, int], ) -> None: super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR) self.id: int = _id self.app: "Application" = app self.manager: "CanvasManager" = manager self.core: "CoreClient" = core - self.selection: dict[int, int] = {} + self.selection: Dict[int, int] = {} self.select_box: Optional[Shape] = None self.selected: Optional[int] = None - self.nodes: dict[int, CanvasNode] = {} - self.shadow_nodes: dict[int, ShadowNode] = {} - self.shapes: dict[int, Shape] = {} - self.shadow_core_nodes: dict[int, ShadowNode] = {} + self.nodes: Dict[int, CanvasNode] = {} + self.shadow_nodes: Dict[int, ShadowNode] = {} + self.shapes: Dict[int, Shape] = {} + self.shadow_core_nodes: Dict[int, ShadowNode] = {} # map wireless/EMANE node to the set of MDRs connected to that node - self.wireless_network: dict[int, set[int]] = {} + self.wireless_network: Dict[int, Set[int]] = {} self.drawing_edge: Optional[CanvasEdge] = None self.rect: Optional[int] = None self.shape_drawing: bool = False - self.current_dimensions: tuple[int, int] = dimensions + self.current_dimensions: Tuple[int, int] = dimensions self.ratio: float = 1.0 - self.offset: tuple[int, int] = (0, 0) - self.cursor: tuple[int, int] = (0, 0) - self.to_copy: list[CanvasNode] = [] + self.offset: Tuple[int, int] = (0, 0) + self.cursor: Tuple[int, int] = (0, 0) + self.to_copy: List[CanvasNode] = [] # background related self.wallpaper_id: Optional[int] = None @@ -82,7 +82,7 @@ class CanvasGraph(tk.Canvas): self.draw_canvas() self.draw_grid() - def draw_canvas(self, dimensions: tuple[int, int] = None) -> None: + def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: if self.rect is not None: self.delete(self.rect) if not dimensions: @@ -126,23 +126,23 @@ class CanvasGraph(tk.Canvas): shadow_node = ShadowNode(self.app, self, node) return shadow_node - def get_actual_coords(self, x: float, y: float) -> tuple[float, float]: + def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y - def get_scaled_coords(self, x: float, y: float) -> tuple[float, float]: + def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y - def inside_canvas(self, x: float, y: float) -> tuple[bool, bool]: + def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y - def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> tuple[bool, bool]: + def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright @@ -161,7 +161,7 @@ class CanvasGraph(tk.Canvas): self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) - def canvas_xy(self, event: tk.Event) -> tuple[float, float]: + def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate """ @@ -516,7 +516,7 @@ class CanvasGraph(tk.Canvas): self.nodes[node.id] = node self.core.set_canvas_node(core_node, node) - def width_and_height(self) -> tuple[int, int]: + def width_and_height(self) -> Tuple[int, int]: """ retrieve canvas width and height in pixels """ @@ -601,7 +601,7 @@ class CanvasGraph(tk.Canvas): self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) - def redraw_canvas(self, dimensions: tuple[int, int] = None) -> None: + def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: logger.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position @@ -814,7 +814,7 @@ class CanvasGraph(tk.Canvas): for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) - def get_metadata(self) -> dict[str, Any]: + def get_metadata(self) -> Dict[str, Any]: wallpaper_path = None if self.wallpaper_file: wallpaper = Path(self.wallpaper_file) @@ -830,7 +830,7 @@ class CanvasGraph(tk.Canvas): dimensions=self.current_dimensions, ) - def parse_metadata(self, config: dict[str, Any]) -> None: + def parse_metadata(self, config: Dict[str, Any]) -> None: fit_image = config.get("fit_image", False) self.adjust_to_dim.set(fit_image) wallpaper_style = config.get("wallpaper_style", 1) diff --git a/daemon/core/gui/graph/manager.py b/daemon/core/gui/graph/manager.py index b2745f5c..dc0adca9 100644 --- a/daemon/core/gui/graph/manager.py +++ b/daemon/core/gui/graph/manager.py @@ -1,10 +1,9 @@ import json import logging import tkinter as tk -from collections.abc import ValuesView from copy import deepcopy from tkinter import BooleanVar, messagebox, ttk -from typing import TYPE_CHECKING, Any, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent from core.gui import nodeutils as nutils @@ -35,7 +34,7 @@ class ShowVar(BooleanVar): self.manager: "CanvasManager" = manager self.tag: str = tag - def state(self) -> Literal["normal", "hidden"]: + def state(self) -> str: return tk.NORMAL if self.get() else tk.HIDDEN def click_handler(self) -> None: @@ -79,14 +78,14 @@ class CanvasManager: self.mode: GraphMode = GraphMode.SELECT self.annotation_type: Optional[ShapeType] = None self.node_draw: Optional[NodeDraw] = None - self.canvases: dict[int, CanvasGraph] = {} + self.canvases: Dict[int, CanvasGraph] = {} # global edge management - self.edges: dict[str, CanvasEdge] = {} - self.wireless_edges: dict[str, CanvasWirelessEdge] = {} + self.edges: Dict[str, CanvasEdge] = {} + self.wireless_edges: Dict[str, CanvasWirelessEdge] = {} # global canvas settings - self.default_dimensions: tuple[int, int] = ( + self.default_dimensions: Tuple[int, int] = ( self.app.guiconfig.preferences.width, self.app.guiconfig.preferences.height, ) @@ -112,8 +111,8 @@ class CanvasManager: # widget self.notebook: Optional[ttk.Notebook] = None - self.canvas_ids: dict[str, int] = {} - self.unique_ids: dict[int, str] = {} + self.canvas_ids: Dict[str, int] = {} + self.unique_ids: Dict[int, str] = {} self.draw() self.setup_bindings() @@ -274,17 +273,17 @@ class CanvasManager: if not self.canvases: self.add_canvas() - def redraw_canvas(self, dimensions: tuple[int, int]) -> None: + def redraw_canvas(self, dimensions: Tuple[int, int]) -> None: canvas = self.current() canvas.redraw_canvas(dimensions) if canvas.wallpaper: canvas.redraw_wallpaper() - def get_metadata(self) -> dict[str, Any]: + def get_metadata(self) -> Dict[str, Any]: canvases = [x.get_metadata() for x in self.all()] return dict(gridlines=self.show_grid.get(), canvases=canvases) - def parse_metadata_canvas(self, metadata: dict[str, Any]) -> None: + def parse_metadata_canvas(self, metadata: Dict[str, Any]) -> None: # canvas setting canvas_config = metadata.get("canvas") logger.debug("canvas metadata: %s", canvas_config) @@ -304,7 +303,7 @@ class CanvasManager: canvas = self.get(canvas_id) canvas.parse_metadata(canvas_config) - def parse_metadata_shapes(self, metadata: dict[str, Any]) -> None: + def parse_metadata_shapes(self, metadata: Dict[str, Any]) -> None: # load saved shapes shapes_config = metadata.get("shapes") if not shapes_config: @@ -314,7 +313,7 @@ class CanvasManager: logger.debug("loading shape: %s", shape_config) Shape.from_metadata(self.app, shape_config) - def parse_metadata_edges(self, metadata: dict[str, Any]) -> None: + def parse_metadata_edges(self, metadata: Dict[str, Any]) -> None: # load edges config edges_config = metadata.get("edges") if not edges_config: @@ -331,7 +330,7 @@ class CanvasManager: else: logger.warning("invalid edge token to configure: %s", edge_token) - def parse_metadata_hidden(self, metadata: dict[str, Any]) -> None: + def parse_metadata_hidden(self, metadata: Dict[str, Any]) -> None: # read hidden nodes hidden_config = metadata.get("hidden") if not hidden_config: diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 0cfbf2e9..b3d0aae9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -2,7 +2,7 @@ import functools import logging import tkinter as tk from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import grpc from PIL.ImageTk import PhotoImage @@ -62,17 +62,17 @@ class CanvasNode: state=self.app.manager.show_node_labels.state(), ) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) - self.edges: set[CanvasEdge] = set() - self.ifaces: dict[int, Interface] = {} - self.wireless_edges: set[CanvasWirelessEdge] = set() - self.antennas: list[int] = [] - self.antenna_images: dict[int, PhotoImage] = {} + self.edges: Set[CanvasEdge] = set() + self.ifaces: Dict[int, Interface] = {} + self.wireless_edges: Set[CanvasWirelessEdge] = set() + self.antennas: List[int] = [] + self.antenna_images: Dict[int, PhotoImage] = {} self.hidden: bool = False self.setup_bindings() self.context: tk.Menu = tk.Menu(self.canvas) themes.style_menu(self.context) - def position(self) -> tuple[int, int]: + def position(self) -> Tuple[int, int]: return self.canvas.coords(self.id) def next_iface_id(self) -> int: @@ -543,7 +543,7 @@ class ShadowNode: self.canvas.shadow_nodes[self.id] = self self.canvas.shadow_core_nodes[self.node.core_node.id] = self - def position(self) -> tuple[int, int]: + def position(self) -> Tuple[int, int]: return self.canvas.coords(self.id) def should_delete(self) -> bool: diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 5f243fdf..7db18b5b 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags @@ -72,7 +72,7 @@ class Shape: self.draw() @classmethod - def from_metadata(cls, app: "Application", config: dict[str, Any]) -> None: + def from_metadata(cls, app: "Application", config: Dict[str, Any]) -> None: shape_type = config["type"] try: shape_type = ShapeType(shape_type) @@ -144,7 +144,7 @@ class Shape: logger.error("unknown shape type: %s", self.shape_type) self.created = True - def get_font(self) -> list[Union[int, str]]: + def get_font(self) -> List[Union[int, str]]: font = [self.shape_data.font, self.shape_data.font_size] if self.shape_data.bold: font.append("bold") @@ -198,7 +198,7 @@ class Shape: self.canvas.delete(self.id) self.canvas.delete(self.text_id) - def metadata(self) -> dict[str, Union[str, int, bool]]: + def metadata(self) -> Dict[str, Union[str, int, bool]]: coords = self.canvas.coords(self.id) # update coords to actual positions if len(coords) == 4: diff --git a/daemon/core/gui/graph/shapeutils.py b/daemon/core/gui/graph/shapeutils.py index ab82ef76..2b62a46c 100644 --- a/daemon/core/gui/graph/shapeutils.py +++ b/daemon/core/gui/graph/shapeutils.py @@ -1,4 +1,5 @@ import enum +from typing import Set class ShapeType(enum.Enum): @@ -8,7 +9,7 @@ class ShapeType(enum.Enum): TEXT = "text" -SHAPES: set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE} +SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE} def is_draw_shape(shape_type: ShapeType) -> bool: diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index cb1ffc15..803b969e 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -1,3 +1,5 @@ +from typing import List + ANNOTATION: str = "annotation" GRIDLINE: str = "gridline" SHAPE: str = "shape" @@ -13,7 +15,7 @@ WALLPAPER: str = "wallpaper" SELECTION: str = "selectednodes" MARKER: str = "marker" HIDDEN: str = "hidden" -ORGANIZE_TAGS: list[str] = [ +ORGANIZE_TAGS: List[str] = [ WALLPAPER, GRIDLINE, SHAPE, @@ -27,7 +29,7 @@ ORGANIZE_TAGS: list[str] = [ SELECTION, MARKER, ] -RESET_TAGS: list[str] = [ +RESET_TAGS: List[str] = [ EDGE, NODE, NODE_LABEL, diff --git a/daemon/core/gui/graph/tooltip.py b/daemon/core/gui/graph/tooltip.py index b820abec..6e4aa62f 100644 --- a/daemon/core/gui/graph/tooltip.py +++ b/daemon/core/gui/graph/tooltip.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Tuple from core.gui.themes import Styles @@ -27,9 +27,9 @@ class CanvasTooltip: self, canvas: "CanvasGraph", *, - pad: tuple[int, int, int, int] = (5, 3, 5, 3), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3), waittime: int = 400, - wraplength: int = 600, + wraplength: int = 600 ) -> None: # in miliseconds, originally 500 self.waittime: int = waittime @@ -37,7 +37,7 @@ class CanvasTooltip: self.wraplength: int = wraplength self.canvas: "CanvasGraph" = canvas self.text: tk.StringVar = tk.StringVar() - self.pad: tuple[int, int, int, int] = pad + self.pad: Tuple[int, int, int, int] = pad self.id: Optional[str] = None self.tw: Optional[tk.Toplevel] = None @@ -63,8 +63,8 @@ class CanvasTooltip: canvas: "CanvasGraph", label: ttk.Label, *, - tip_delta: tuple[int, int] = (10, 5), - pad: tuple[int, int, int, int] = (5, 3, 5, 3), + tip_delta: Tuple[int, int] = (10, 5), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3) ): c = canvas s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() @@ -112,7 +112,7 @@ class CanvasTooltip: ) label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) x, y = tip_pos_calculator(canvas, label, pad=pad) - self.tw.wm_geometry(f"+{x:d}+{y:d}") + self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self) -> None: if self.tw: diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 070137fb..112f3943 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional +from typing import Dict, Optional, Tuple from PIL import Image from PIL.ImageTk import PhotoImage @@ -12,7 +12,7 @@ ANTENNA_SIZE: int = 32 BUTTON_SIZE: int = 16 ERROR_SIZE: int = 24 DIALOG_SIZE: int = 16 -IMAGES: dict[str, str] = {} +IMAGES: Dict[str, str] = {} def load_all() -> None: @@ -78,7 +78,6 @@ class ImageEnum(Enum): EDITDELETE = "edit-delete" ANTENNA = "antenna" DOCKER = "docker" - PODMAN = "podman" LXC = "lxc" ALERT = "alert" DELETE = "delete" @@ -88,22 +87,21 @@ class ImageEnum(Enum): SHADOW = "shadow" -TYPE_MAP: dict[tuple[NodeType, str], ImageEnum] = { +TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = { (NodeType.DEFAULT, "router"): ImageEnum.ROUTER, (NodeType.DEFAULT, "PC"): ImageEnum.PC, (NodeType.DEFAULT, "host"): ImageEnum.HOST, (NodeType.DEFAULT, "mdr"): ImageEnum.MDR, (NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, - (NodeType.HUB, None): ImageEnum.HUB, - (NodeType.SWITCH, None): ImageEnum.SWITCH, - (NodeType.WIRELESS_LAN, None): ImageEnum.WLAN, - (NodeType.WIRELESS, None): ImageEnum.WIRELESS, - (NodeType.EMANE, None): ImageEnum.EMANE, - (NodeType.RJ45, None): ImageEnum.RJ45, - (NodeType.TUNNEL, None): ImageEnum.TUNNEL, - (NodeType.DOCKER, None): ImageEnum.DOCKER, - (NodeType.PODMAN, None): ImageEnum.PODMAN, - (NodeType.LXC, None): ImageEnum.LXC, + (NodeType.HUB, ""): ImageEnum.HUB, + (NodeType.SWITCH, ""): ImageEnum.SWITCH, + (NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, + (NodeType.WIRELESS, ""): ImageEnum.WIRELESS, + (NodeType.EMANE, ""): ImageEnum.EMANE, + (NodeType.RJ45, ""): ImageEnum.RJ45, + (NodeType.TUNNEL, ""): ImageEnum.TUNNEL, + (NodeType.DOCKER, ""): ImageEnum.DOCKER, + (NodeType.LXC, ""): ImageEnum.LXC, } diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 9ebea3c1..83fba104 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork @@ -43,7 +43,7 @@ class Subnets: def __hash__(self) -> int: return hash(self.key()) - def key(self) -> tuple[IPNetwork, IPNetwork]: + def key(self) -> Tuple[IPNetwork, IPNetwork]: return self.ip4, self.ip6 def next(self) -> "Subnets": @@ -61,8 +61,8 @@ class InterfaceManager: self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded) self.current_mac: Optional[EUI] = None self.current_subnets: Optional[Subnets] = None - self.used_subnets: dict[tuple[IPNetwork, IPNetwork], Subnets] = {} - self.used_macs: set[str] = set() + self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {} + self.used_macs: Set[str] = set() def update_ips(self, ip4: str, ip6: str) -> None: self.reset() @@ -91,7 +91,7 @@ class InterfaceManager: self.current_subnets = None self.used_subnets.clear() - def removed(self, links: list[Link]) -> None: + def removed(self, links: List[Link]) -> None: # get remaining subnets remaining_subnets = set() for edge in self.app.core.links.values(): @@ -121,7 +121,7 @@ class InterfaceManager: subnets.used_indexes.discard(index) self.current_subnets = None - def set_macs(self, links: list[Link]) -> None: + def set_macs(self, links: List[Link]) -> None: self.current_mac = self.mac self.used_macs.clear() for link in links: @@ -130,7 +130,7 @@ class InterfaceManager: if link.iface2: self.used_macs.add(link.iface2.mac) - def joined(self, links: list[Link]) -> None: + def joined(self, links: List[Link]) -> None: ifaces = [] for link in links: if link.iface1: @@ -208,7 +208,7 @@ class InterfaceManager: logger.info("ignoring subnet change for link between network nodes") def find_subnets( - self, canvas_node: CanvasNode, visited: set[int] = None + self, canvas_node: CanvasNode, visited: Set[int] = None ) -> Optional[IPNetwork]: logger.info("finding subnet for node: %s", canvas_node.core_node.name) subnets = None diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 16e57cb6..e2df2f92 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -235,11 +235,7 @@ class Menubar(tk.Menu): menu.add_command( label="Configure Throughput", command=self.click_config_throughput ) - menu.add_checkbutton( - label="Enable Throughput?", - command=self.click_throughput, - variable=self.core.show_throughputs, - ) + menu.add_checkbutton(label="Enable Throughput?", command=self.click_throughput) widget_menu.add_cascade(label="Throughput", menu=menu) def draw_widgets_menu(self) -> None: @@ -397,7 +393,7 @@ class Menubar(tk.Menu): dialog.show() def click_throughput(self) -> None: - if self.core.show_throughputs.get(): + if not self.core.handling_throughputs: self.core.enable_throughputs() else: self.core.cancel_throughputs() diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 0b3e3d9a..0175797d 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional, Set from PIL.ImageTk import PhotoImage @@ -13,27 +13,22 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.gui.app import Application -NODES: list["NodeDraw"] = [] -NETWORK_NODES: list["NodeDraw"] = [] +NODES: List["NodeDraw"] = [] +NETWORK_NODES: List["NodeDraw"] = [] NODE_ICONS = {} -CONTAINER_NODES: set[NodeType] = { - NodeType.DEFAULT, - NodeType.DOCKER, - NodeType.LXC, - NodeType.PODMAN, -} -IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC, NodeType.PODMAN} -WIRELESS_NODES: set[NodeType] = { +CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} +IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} +WIRELESS_NODES: Set[NodeType] = { NodeType.WIRELESS_LAN, NodeType.EMANE, NodeType.WIRELESS, } -RJ45_NODES: set[NodeType] = {NodeType.RJ45} -BRIDGE_NODES: set[NodeType] = {NodeType.HUB, NodeType.SWITCH} -IGNORE_NODES: set[NodeType] = {NodeType.CONTROL_NET} -MOBILITY_NODES: set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} -NODE_MODELS: set[str] = {"router", "PC", "mdr", "prouter"} -ROUTER_NODES: set[str] = {"router", "mdr"} +RJ45_NODES: Set[NodeType] = {NodeType.RJ45} +BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH} +IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} +MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} +NODE_MODELS: Set[str] = {"router", "PC", "mdr", "prouter"} +ROUTER_NODES: Set[str] = {"router", "mdr"} ANTENNA_ICON: Optional[PhotoImage] = None @@ -46,7 +41,6 @@ def setup() -> None: (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), (ImageEnum.LXC, NodeType.LXC, "LXC", None), - (ImageEnum.PODMAN, NodeType.PODMAN, "Podman", None), ] for image_enum, node_type, label, model in nodes: node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) @@ -112,7 +106,7 @@ def is_iface_node(node: Node) -> bool: return is_container(node) or is_bridge(node) -def get_custom_services(gui_config: GuiConfig, name: str) -> list[str]: +def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]: for custom_node in gui_config.nodes: if custom_node.name == name: return custom_node.services @@ -129,7 +123,7 @@ def _get_custom_file(config: GuiConfig, name: str) -> Optional[str]: def get_icon(node: Node, app: "Application") -> PhotoImage: scale = app.app_scale image = None - # node icon was overridden with a specific value + # node icon was overriden with a specific value if node.icon: try: image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale) @@ -160,7 +154,7 @@ class NodeDraw: self.image_file: Optional[str] = None self.node_type: Optional[NodeType] = None self.model: Optional[str] = None - self.services: set[str] = set() + self.services: Set[str] = set() self.label: Optional[str] = None @classmethod diff --git a/daemon/core/gui/observers.py b/daemon/core/gui/observers.py index 8cf026bd..7879494b 100644 --- a/daemon/core/gui/observers.py +++ b/daemon/core/gui/observers.py @@ -1,13 +1,13 @@ import tkinter as tk from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from core.gui.dialogs.observers import ObserverDialog if TYPE_CHECKING: from core.gui.app import Application -OBSERVERS: dict[str, str] = { +OBSERVERS: Dict[str, str] = { "List Processes": "ps", "Show Interfaces": "ip address", "IPV4 Routes": "ip -4 route", diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index a4967cd6..441213f2 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -3,7 +3,7 @@ status bar """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel from core.gui.dialogs.alerts import AlertsDialog @@ -24,7 +24,7 @@ class StatusBar(ttk.Frame): self.alerts_button: Optional[ttk.Button] = None self.alert_style = Styles.no_alert self.running: bool = False - self.core_alarms: list[ExceptionEvent] = [] + self.core_alarms: List[ExceptionEvent] = [] self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 6bbeb70f..2623136d 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -2,7 +2,7 @@ import logging import threading import time import tkinter as tk -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ class ProgressTask: title: str, task: Callable, callback: Callable = None, - args: tuple[Any] = None, + args: Tuple[Any] = None, ): self.app: "Application" = app self.title: str = title @@ -25,7 +25,7 @@ class ProgressTask: self.callback: Callable = callback if args is None: args = () - self.args: tuple[Any] = args + self.args: Tuple[Any] = args self.time: Optional[float] = None def start(self) -> None: diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index cb6280e5..45b109f0 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -1,9 +1,10 @@ import tkinter as tk from tkinter import font, ttk +from typing import Dict, Tuple THEME_DARK: str = "black" -PADX: tuple[int, int] = (0, 5) -PADY: tuple[int, int] = (0, 5) +PADX: Tuple[int, int] = (0, 5) +PADY: Tuple[int, int] = (0, 5) FRAME_PAD: int = 5 DIALOG_PAD: int = 5 @@ -200,7 +201,7 @@ def theme_change(event: tk.Event) -> None: _alert_style(style, Styles.red_alert, "red") -def scale_fonts(fonts_size: dict[str, int], scale: float) -> None: +def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None: for name in font.names(): f = font.nametofont(name) if name in fonts_size: diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 7c32c0af..7392071d 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -3,7 +3,7 @@ import tkinter as tk from enum import Enum from functools import partial from tkinter import ttk -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Callable, List, Optional from PIL.ImageTk import PhotoImage @@ -90,7 +90,7 @@ class ButtonBar(ttk.Frame): def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) self.app: "Application" = app - self.radio_buttons: list[ttk.Button] = [] + self.radio_buttons: List[ttk.Button] = [] def create_button( self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False @@ -303,7 +303,7 @@ class Toolbar(ttk.Frame): ) task.start() - def start_callback(self, result: bool, exceptions: list[str]) -> None: + def start_callback(self, result: bool, exceptions: List[str]) -> None: self.set_runtime() self.app.core.show_mobility_players() if not result and exceptions: diff --git a/daemon/core/gui/tooltip.py b/daemon/core/gui/tooltip.py index 6d84ac75..84a3178f 100644 --- a/daemon/core/gui/tooltip.py +++ b/daemon/core/gui/tooltip.py @@ -5,7 +5,7 @@ from typing import Optional from core.gui.themes import Styles -class Tooltip: +class Tooltip(object): """ Create tool tip for a given widget """ @@ -42,7 +42,7 @@ class Tooltip: y += self.widget.winfo_rooty() + 32 self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) - self.tw.wm_geometry(f"+{x:d}+{y:d}") + self.tw.wm_geometry("+%d+%d" % (x, y)) self.tw.rowconfigure(0, weight=1) self.tw.columnconfigure(0, weight=1) frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index 61500e84..2360ab0b 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -3,9 +3,8 @@ input validation """ import re import tkinter as tk -from re import Pattern from tkinter import ttk -from typing import Any, Optional +from typing import Any, Optional, Pattern SMALLEST_SCALE: float = 0.5 LARGEST_SCALE: float = 5.0 diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 902f1132..7dfd2666 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -3,7 +3,7 @@ import tkinter as tk from functools import partial from pathlib import Path from tkinter import filedialog, font, ttk -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type from core.api.grpc.wrappers import ConfigOption, ConfigOptionType from core.gui import appconfig, themes, validation @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.gui.app import Application -INT_TYPES: set[ConfigOptionType] = { +INT_TYPES: Set[ConfigOptionType] = { ConfigOptionType.UINT8, ConfigOptionType.UINT16, ConfigOptionType.UINT32, @@ -40,7 +40,7 @@ class FrameScroll(ttk.Frame): self, master: tk.Widget, app: "Application", - _cls: type[ttk.Frame] = ttk.Frame, + _cls: Type[ttk.Frame] = ttk.Frame, **kw: Any ) -> None: super().__init__(master, **kw) @@ -86,14 +86,14 @@ class ConfigFrame(ttk.Notebook): self, master: tk.Widget, app: "Application", - config: dict[str, ConfigOption], + config: Dict[str, ConfigOption], enabled: bool = True, **kw: Any ) -> None: super().__init__(master, **kw) self.app: "Application" = app - self.config: dict[str, ConfigOption] = config - self.values: dict[str, tk.StringVar] = {} + self.config: Dict[str, ConfigOption] = config + self.values: Dict[str, tk.StringVar] = {} self.enabled: bool = enabled def draw_config(self) -> None: @@ -166,7 +166,7 @@ class ConfigFrame(ttk.Notebook): logger.error("unhandled config option type: %s", option.type) self.values[option.name] = value - def parse_config(self) -> dict[str, str]: + def parse_config(self) -> Dict[str, str]: for key in self.config: option = self.config[key] value = self.values[key] @@ -180,7 +180,7 @@ class ConfigFrame(ttk.Notebook): option.value = config_value return {x: self.config[x].value for x in self.config} - def set_values(self, config: dict[str, str]) -> None: + def set_values(self, config: Dict[str, str]) -> None: for name, data in config.items(): option = self.config[name] value = self.values[name] diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 9b300241..7f8a33a1 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -6,7 +6,7 @@ import heapq import threading import time from functools import total_ordering -from typing import Any, Callable, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple class Timer(threading.Thread): @@ -19,8 +19,8 @@ class Timer(threading.Thread): self, interval: float, func: Callable[..., None], - args: tuple[Any] = None, - kwargs: dict[Any, Any] = None, + args: Tuple[Any] = None, + kwargs: Dict[Any, Any] = None, ) -> None: """ Create a Timer instance. @@ -38,11 +38,11 @@ class Timer(threading.Thread): # validate arguments were provided if args is None: args = () - self.args: tuple[Any] = args + self.args: Tuple[Any] = args # validate keyword arguments were provided if kwargs is None: kwargs = {} - self.kwargs: dict[Any, Any] = kwargs + self.kwargs: Dict[Any, Any] = kwargs def cancel(self) -> bool: """ @@ -96,8 +96,8 @@ class Event: self.eventnum: int = eventnum self.time: float = event_time self.func: Callable[..., None] = func - self.args: tuple[Any] = args - self.kwds: dict[Any, Any] = kwds + self.args: Tuple[Any] = args + self.kwds: Dict[Any, Any] = kwds self.canceled: bool = False def __lt__(self, other: "Event") -> bool: @@ -135,7 +135,7 @@ class EventLoop: Creates a EventLoop instance. """ self.lock: threading.RLock = threading.RLock() - self.queue: list[Event] = [] + self.queue: List[Event] = [] self.eventnum: int = 0 self.timer: Optional[Timer] = None self.running: bool = False diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index 78308728..5896e074 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -3,6 +3,7 @@ Provides conversions from x,y,z to lon,lat,alt. """ import logging +from typing import Tuple import pyproj from pyproj import Transformer @@ -34,9 +35,9 @@ class GeoLocation: self.to_geo: Transformer = pyproj.Transformer.from_crs( CRS_PROJ, CRS_WGS84, always_xy=True ) - self.refproj: tuple[float, float, float] = (0.0, 0.0, 0.0) - self.refgeo: tuple[float, float, float] = (0.0, 0.0, 0.0) - self.refxyz: tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refproj: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refgeo: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refxyz: Tuple[float, float, float] = (0.0, 0.0, 0.0) self.refscale: float = 1.0 def setrefgeo(self, lat: float, lon: float, alt: float) -> None: @@ -83,7 +84,7 @@ class GeoLocation: return 0.0 return SCALE_FACTOR * (value / self.refscale) - def getxyz(self, lat: float, lon: float, alt: float) -> tuple[float, float, float]: + def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]: """ Convert provided lon,lat,alt to x,y,z. @@ -103,7 +104,7 @@ class GeoLocation: logger.debug("result x,y,z(%s, %s, %s)", x, y, z) return x, y, z - def getgeo(self, x: float, y: float, z: float) -> tuple[float, float, float]: + def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: """ Convert provided x,y,z to lon,lat,alt. diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index ebac9bc5..28040650 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -9,7 +9,7 @@ import threading import time from functools import total_ordering from pathlib import Path -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from core import utils from core.config import ( @@ -47,7 +47,7 @@ def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, Emane return session.get_node(node_id, EmaneNet) -def get_config_int(current: int, config: dict[str, str], name: str) -> Optional[int]: +def get_config_int(current: int, config: Dict[str, str], name: str) -> Optional[int]: """ Convenience function to get config values as int. @@ -63,7 +63,7 @@ def get_config_int(current: int, config: dict[str, str], name: str) -> Optional[ def get_config_float( - current: Union[int, float], config: dict[str, str], name: str + current: Union[int, float], config: Dict[str, str], name: str ) -> Optional[float]: """ Convenience function to get config values as float. @@ -112,7 +112,7 @@ class MobilityManager(ModelManager): """ self.config_reset() - def startup(self, node_ids: list[int] = None) -> None: + def startup(self, node_ids: List[int] = None) -> None: """ Session is transitioning from instantiation to runtime state. Instantiate any mobility models that have been configured for a WLAN. @@ -237,7 +237,7 @@ class WirelessModel(ConfigurableOptions): self.session: "Session" = session self.id: int = _id - def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ May be used if the model can populate the GUI with wireless (green) link lines. @@ -247,7 +247,7 @@ class WirelessModel(ConfigurableOptions): """ return [] - def update(self, moved_ifaces: list[CoreInterface]) -> None: + def update(self, moved_ifaces: List[CoreInterface]) -> None: """ Update this wireless model. @@ -256,7 +256,7 @@ class WirelessModel(ConfigurableOptions): """ raise NotImplementedError - def update_config(self, config: dict[str, str]) -> None: + def update_config(self, config: Dict[str, str]) -> None: """ For run-time updates of model config. Returns True when position callback and set link parameters should be invoked. @@ -275,7 +275,7 @@ class BasicRangeModel(WirelessModel): """ name: str = "basic_range" - options: list[Configuration] = [ + options: List[Configuration] = [ ConfigInt(id="range", default="275", label="wireless range (pixels)"), ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"), ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"), @@ -298,7 +298,7 @@ class BasicRangeModel(WirelessModel): super().__init__(session, _id) self.session: "Session" = session self.wlan: WlanNode = session.get_node(_id, WlanNode) - self.iface_to_pos: dict[CoreInterface, tuple[float, float, float]] = {} + self.iface_to_pos: Dict[CoreInterface, Tuple[float, float, float]] = {} self.iface_lock: threading.Lock = threading.Lock() self.range: int = 0 self.bw: Optional[int] = None @@ -323,7 +323,7 @@ class BasicRangeModel(WirelessModel): iface.options.update(options) iface.set_config() - def get_position(self, iface: CoreInterface) -> tuple[float, float, float]: + def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: """ Retrieve network interface position. @@ -352,7 +352,7 @@ class BasicRangeModel(WirelessModel): position_callback = set_position - def update(self, moved_ifaces: list[CoreInterface]) -> None: + def update(self, moved_ifaces: List[CoreInterface]) -> None: """ Node positions have changed without recalc. Update positions from node.position, then re-calculate links for those that have moved. @@ -412,7 +412,7 @@ class BasicRangeModel(WirelessModel): @staticmethod def calcdistance( - p1: tuple[float, float, float], p2: tuple[float, float, float] + p1: Tuple[float, float, float], p2: Tuple[float, float, float] ) -> float: """ Calculate the distance between two three-dimensional points. @@ -428,7 +428,7 @@ class BasicRangeModel(WirelessModel): c = p1[2] - p2[2] return math.hypot(math.hypot(a, b), c) - def update_config(self, config: dict[str, str]) -> None: + def update_config(self, config: Dict[str, str]) -> None: """ Configuration has changed during runtime. @@ -487,7 +487,7 @@ class BasicRangeModel(WirelessModel): link_data = self.create_link_data(iface, iface2, message_type) self.session.broadcast_link(link_data) - def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Return a list of wireless link messages for when the GUI reconnects. @@ -513,7 +513,7 @@ class WayPoint: self, _time: float, node_id: int, - coords: tuple[float, float, Optional[float]], + coords: Tuple[float, float, Optional[float]], speed: float, ) -> None: """ @@ -526,7 +526,7 @@ class WayPoint: """ self.time: float = _time self.node_id: int = node_id - self.coords: tuple[float, float, Optional[float]] = coords + self.coords: Tuple[float, float, Optional[float]] = coords self.speed: float = speed def __eq__(self, other: "WayPoint") -> bool: @@ -563,10 +563,10 @@ class WayPointMobility(WirelessModel): """ super().__init__(session=session, _id=_id) self.state: int = self.STATE_STOPPED - self.queue: list[WayPoint] = [] - self.queue_copy: list[WayPoint] = [] - self.points: dict[int, WayPoint] = {} - self.initial: dict[int, WayPoint] = {} + self.queue: List[WayPoint] = [] + self.queue_copy: List[WayPoint] = [] + self.points: Dict[int, WayPoint] = {} + self.initial: Dict[int, WayPoint] = {} self.lasttime: Optional[float] = None self.endtime: Optional[int] = None self.timezero: float = 0.0 @@ -855,7 +855,7 @@ class Ns2ScriptedMobility(WayPointMobility): """ name: str = "ns2script" - options: list[Configuration] = [ + options: List[Configuration] = [ ConfigString(id="file", label="mobility script file"), ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"), ConfigBool(id="loop", default="1", label="loop"), @@ -867,7 +867,7 @@ class Ns2ScriptedMobility(WayPointMobility): ] @classmethod - def config_groups(cls) -> list[ConfigGroup]: + def config_groups(cls) -> List[ConfigGroup]: return [ ConfigGroup("ns-2 Mobility Script Parameters", 1, len(cls.configurations())) ] @@ -882,12 +882,12 @@ class Ns2ScriptedMobility(WayPointMobility): super().__init__(session, _id) self.file: Optional[Path] = None self.autostart: Optional[str] = None - self.nodemap: dict[int, int] = {} + self.nodemap: Dict[int, int] = {} self.script_start: Optional[str] = None self.script_pause: Optional[str] = None self.script_stop: Optional[str] = None - def update_config(self, config: dict[str, str]) -> None: + def update_config(self, config: Dict[str, str]) -> None: self.file = Path(config["file"]) logger.info( "ns-2 scripted mobility configured for WLAN %d using file: %s", @@ -916,7 +916,7 @@ class Ns2ScriptedMobility(WayPointMobility): file_path = self.findfile(self.file) try: f = file_path.open("r") - except OSError: + except IOError: logger.exception( "ns-2 scripted mobility failed to load file: %s", self.file ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index e59a89e4..7edbc198 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -9,7 +9,7 @@ import threading from dataclasses import dataclass, field from pathlib import Path from threading import RLock -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import netaddr @@ -29,10 +29,10 @@ if TYPE_CHECKING: from core.configservice.base import ConfigService from core.services.coreservices import CoreService - CoreServices = list[Union[CoreService, type[CoreService]]] - ConfigServiceType = type[ConfigService] + CoreServices = List[Union[CoreService, Type[CoreService]]] + ConfigServiceType = Type[ConfigService] -PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")] +PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")] @dataclass @@ -64,7 +64,7 @@ class Position: self.z = z return True - def get(self) -> tuple[float, float, float]: + def get(self) -> Tuple[float, float, float]: """ Retrieve x,y,z position. @@ -88,7 +88,7 @@ class Position: self.lat = lat self.alt = alt - def get_geo(self) -> tuple[float, float, float]: + def get_geo(self) -> Tuple[float, float, float]: """ Retrieve current geo position lon, lat, alt. @@ -113,9 +113,9 @@ class NodeOptions: class CoreNodeOptions(NodeOptions): model: str = "PC" """model is used for providing a default set of services""" - services: list[str] = field(default_factory=list) + services: List[str] = field(default_factory=list) """services to start within node""" - config_services: list[str] = field(default_factory=list) + config_services: List[str] = field(default_factory=list) """config services to start within node""" directory: Path = None """directory to define node, defaults to path under the session directory""" @@ -152,7 +152,7 @@ class NodeBase(abc.ABC): self.server: "DistributedServer" = server self.model: Optional[str] = None self.services: CoreServices = [] - self.ifaces: dict[int, CoreInterface] = {} + self.ifaces: Dict[int, CoreInterface] = {} self.iface_id: int = 0 self.position: Position = Position() self.up: bool = False @@ -201,7 +201,7 @@ class NodeBase(abc.ABC): def host_cmd( self, args: str, - env: dict[str, str] = None, + env: Dict[str, str] = None, cwd: Path = None, wait: bool = True, shell: bool = False, @@ -246,7 +246,7 @@ class NodeBase(abc.ABC): """ return self.position.set(x=x, y=y, z=z) - def getposition(self) -> tuple[float, float, float]: + def getposition(self) -> Tuple[float, float, float]: """ Return an (x,y,z) tuple representing this object's position. @@ -276,9 +276,8 @@ class NodeBase(abc.ABC): mtu = DEFAULT_MTU if iface_data and iface_data.mtu is not None: mtu = iface_data.mtu - unique_name = f"{self.id}.{iface_id}.{self.session.short_session_id()}" - name = f"veth{unique_name}" - localname = f"beth{unique_name}" + name = f"veth{self.id}.{iface_id}.{self.session.short_session_id()}" + localname = f"{name}p" iface = CoreInterface( iface_id, name, @@ -331,7 +330,7 @@ class NodeBase(abc.ABC): raise CoreError(f"node({self.name}) does not have interface({iface_id})") return self.ifaces[iface_id] - def get_ifaces(self, control: bool = True) -> list[CoreInterface]: + def get_ifaces(self, control: bool = True) -> List[CoreInterface]: """ Retrieve sorted list of interfaces, optionally do not include control interfaces. @@ -395,7 +394,7 @@ class CoreNodeBase(NodeBase): will run on, default is None for localhost """ super().__init__(session, _id, name, server, options) - self.config_services: dict[str, "ConfigService"] = {} + self.config_services: Dict[str, "ConfigService"] = {} self.directory: Optional[Path] = None self.tmpnodedir: bool = False @@ -481,7 +480,7 @@ class CoreNodeBase(NodeBase): raise CoreError(f"node({self.name}) already has service({name})") self.config_services[name] = service_class(self) - def set_service_config(self, name: str, data: dict[str, str]) -> None: + def set_service_config(self, name: str, data: Dict[str, str]) -> None: """ Sets configuration service custom config data. @@ -506,15 +505,6 @@ class CoreNodeBase(NodeBase): for service in startup_path: service.start() - def stop_config_services(self) -> None: - """ - Stop all configuration services. - - :return: nothing - """ - for service in self.config_services.values(): - service.stop() - def makenodedir(self) -> None: """ Create the node directory. @@ -583,7 +573,7 @@ class CoreNode(CoreNodeBase): self.directory: Optional[Path] = options.directory self.ctrlchnlname: Path = self.session.directory / self.name self.pid: Optional[int] = None - self._mounts: list[tuple[Path, Path]] = [] + self._mounts: List[Tuple[Path, Path]] = [] self.node_net_client: LinuxNetClient = self.create_node_net_client( self.session.use_ovs() ) @@ -901,7 +891,7 @@ class CoreNode(CoreNodeBase): for ip in iface.ips(): # ipv4 check broadcast = None - if netaddr.valid_ipv4(str(ip.ip)): + if netaddr.valid_ipv4(ip): broadcast = "+" self.node_net_client.create_address(iface.name, str(ip), broadcast) # configure iface options @@ -937,7 +927,7 @@ class CoreNetworkBase(NodeBase): mtu = self.session.options.get_int("mtu") self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU self.brname: Optional[str] = None - self.linked: dict[CoreInterface, dict[CoreInterface, bool]] = {} + self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} self.linked_lock: threading.Lock = threading.Lock() def attach(self, iface: CoreInterface) -> None: diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index ad05c407..45d2b892 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -4,9 +4,8 @@ import shlex from dataclasses import dataclass, field from pathlib import Path from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Tuple -from core import utils from core.emulator.distributed import DistributedServer from core.errors import CoreCommandError, CoreError from core.executables import BASH @@ -24,9 +23,9 @@ DOCKER: str = "docker" class DockerOptions(CoreNodeOptions): image: str = "ubuntu" """image used when creating container""" - binds: list[tuple[str, str]] = field(default_factory=list) + binds: List[Tuple[str, str]] = field(default_factory=list) """bind mount source and destinations to setup within container""" - volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list) + volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) """ volume mount source, destination, unique, delete to setup within container @@ -75,9 +74,8 @@ class DockerNode(CoreNode): options = options or DockerOptions() super().__init__(session, _id, name, server, options) self.image: str = options.image - self.binds: list[tuple[str, str]] = options.binds - self.volumes: dict[str, DockerVolume] = {} - self.env: dict[str, str] = {} + self.binds: List[Tuple[str, str]] = options.binds + self.volumes: Dict[str, DockerVolume] = {} for src, dst, unique, delete in options.volumes: src_name = self._unique_name(src) if unique else src self.volumes[src] = DockerVolume(src_name, dst, unique, delete) @@ -101,24 +99,7 @@ class DockerNode(CoreNode): """ if shell: args = f"{BASH} -c {shlex.quote(args)}" - return f"nsenter -t {self.pid} -m -u -i -p -n -- {args}" - - def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - """ - Runs a command that is used to configure and setup the network within a - node. - - :param args: command to run - :param wait: True to wait for status, False otherwise - :param shell: True to use shell, False otherwise - :return: combined stdout and stderr - :raises CoreCommandError: when a non-zero exit status occurs - """ - args = self.create_cmd(args, shell) - if self.server is None: - return utils.cmd(args, wait=wait, shell=shell, env=self.env) - else: - return self.server.remote_cmd(args, wait=wait, env=self.env) + return f"nsenter -t {self.pid} -m -u -i -p -n {args}" def _unique_name(self, name: str) -> str: """ @@ -152,9 +133,7 @@ class DockerNode(CoreNode): with self.lock: if self.up: raise CoreError(f"starting node({self.name}) that is already up") - # create node directory self.makenodedir() - # setup commands for creating bind/volume mounts binds = "" for src, dst in self.binds: binds += f"--mount type=bind,source={src},target={dst} " @@ -163,26 +142,16 @@ class DockerNode(CoreNode): volumes += ( f"--mount type=volume," f"source={volume.src},target={volume.dst} " ) - # normalize hostname hostname = self.name.replace("_", "-") - # create container and retrieve the created containers PID self.host_cmd( f"{DOCKER} run -td --init --net=none --hostname {hostname} " f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 " f"{binds} {volumes} " f"--privileged {self.image} tail -f /dev/null" ) - # retrieve pid and process environment for use in nsenter commands self.pid = self.host_cmd( f"{DOCKER} inspect -f '{{{{.State.Pid}}}}' {self.name}" ) - output = self.host_cmd(f"cat /proc/{self.pid}/environ") - for line in output.split("\x00"): - if not line: - continue - key, value = line.split("=") - self.env[key] = value - # setup symlinks for bind and volume mounts within for src, dst in self.binds: link_path = self.host_path(Path(dst), True) self.host_cmd(f"ln -s {src} {link_path}") @@ -258,7 +227,7 @@ class DockerNode(CoreNode): """ logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) temp = NamedTemporaryFile(delete=False) - temp.write(contents.encode()) + temp.write(contents.encode("utf-8")) temp.close() temp_path = Path(temp.name) directory = file_path.parent diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 294e85f9..bb90653f 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -5,7 +5,7 @@ virtual ethernet classes that implement the interfaces available under Linux. import logging import math from pathlib import Path -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Callable, Dict, List, Optional import netaddr @@ -114,8 +114,8 @@ class CoreInterface: self.up: bool = False self.mtu: int = mtu self.net: Optional[CoreNetworkBase] = None - self.ip4s: list[netaddr.IPNetwork] = [] - self.ip6s: list[netaddr.IPNetwork] = [] + self.ip4s: List[netaddr.IPNetwork] = [] + self.ip6s: List[netaddr.IPNetwork] = [] self.mac: Optional[netaddr.EUI] = None # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None @@ -133,7 +133,7 @@ class CoreInterface: def host_cmd( self, args: str, - env: dict[str, str] = None, + env: Dict[str, str] = None, cwd: Path = None, wait: bool = True, shell: bool = False, @@ -235,7 +235,7 @@ class CoreInterface: """ return next(iter(self.ip6s), None) - def ips(self) -> list[netaddr.IPNetwork]: + def ips(self) -> List[netaddr.IPNetwork]: """ Retrieve a list of all ip4 and ip6 addresses combined. diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index e4cba002..01bd2db7 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -5,7 +5,7 @@ import time from dataclasses import dataclass, field from pathlib import Path from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Tuple from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer @@ -24,9 +24,9 @@ if TYPE_CHECKING: class LxcOptions(CoreNodeOptions): image: str = "ubuntu" """image used when creating container""" - binds: list[tuple[str, str]] = field(default_factory=list) + binds: List[Tuple[str, str]] = field(default_factory=list) """bind mount source and destinations to setup within container""" - volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list) + volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) """ volume mount source, destination, unique, delete to setup within container @@ -74,7 +74,7 @@ class LxcNode(CoreNode): args = f"{BASH} -c {shlex.quote(args)}" return f"nsenter -t {self.pid} -m -u -i -p -n {args}" - def _get_info(self) -> dict: + def _get_info(self) -> Dict: args = f"lxc list {self.name} --format json" output = self.host_cmd(args) data = json.loads(output) @@ -170,7 +170,7 @@ class LxcNode(CoreNode): """ logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) temp = NamedTemporaryFile(delete=False) - temp.write(contents.encode()) + temp.write(contents.encode("utf-8")) temp.close() temp_path = Path(temp.name) directory = file_path.parent diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 74087e31..e0a409f4 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -5,7 +5,6 @@ from typing import Callable import netaddr -from core import utils from core.executables import ETHTOOL, IP, OVS_VSCTL, SYSCTL, TC @@ -178,7 +177,6 @@ class LinuxNetClient: if netaddr.valid_ipv6(address.split("/")[0]): # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed - device = utils.sysctl_devname(device) self.run(f"{SYSCTL} -w net.ipv6.conf.{device}.keep_addr_on_down=1") def delete_address(self, device: str, address: str) -> None: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 1ea9c31e..2b52cbdf 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -6,7 +6,7 @@ import logging import threading from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Type import netaddr @@ -51,7 +51,7 @@ class NftablesQueue: # this lock protects cmds and updates lists self.lock: threading.Lock = threading.Lock() # list of pending nftables commands - self.cmds: list[str] = [] + self.cmds: List[str] = [] # list of WLANs requiring update self.updates: utils.SetQueue = utils.SetQueue() @@ -226,7 +226,7 @@ class CoreNetwork(CoreNetworkBase): def host_cmd( self, args: str, - env: dict[str, str] = None, + env: Dict[str, str] = None, cwd: Path = None, wait: bool = True, shell: bool = False, @@ -448,7 +448,7 @@ class GreTapBridge(CoreNetwork): self.gretap = None super().shutdown() - def add_ips(self, ips: list[str]) -> None: + def add_ips(self, ips: List[str]) -> None: """ Set the remote tunnel endpoint. This is a one-time method for creating the GreTap device, which requires the remoteip at startup. @@ -512,7 +512,7 @@ class CtrlNet(CoreNetwork): policy: NetworkPolicy = NetworkPolicy.ACCEPT # base control interface index CTRLIF_IDX_BASE: int = 99 - DEFAULT_PREFIX_LIST: list[str] = [ + DEFAULT_PREFIX_LIST: List[str] = [ "172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24", "172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24", "172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24", @@ -647,15 +647,6 @@ class PtpNet(CoreNetwork): raise CoreError("ptp links support at most 2 network interfaces") super().attach(iface) - def startup(self) -> None: - """ - Startup for a p2p node, that disables mac learning after normal startup. - - :return: nothing - """ - super().startup() - self.net_client.set_mac_learning(self.brname, LEARNING_DISABLED) - class SwitchNode(CoreNetwork): """ @@ -734,7 +725,7 @@ class WlanNode(CoreNetwork): iface.poshook = self.wireless_model.position_callback iface.setposition() - def setmodel(self, wireless_model: type["WirelessModel"], config: dict[str, str]): + def setmodel(self, wireless_model: Type["WirelessModel"], config: Dict[str, str]): """ Sets the mobility and wireless model. @@ -753,12 +744,12 @@ class WlanNode(CoreNetwork): self.mobility = wireless_model(session=self.session, _id=self.id) self.mobility.update_config(config) - def update_mobility(self, config: dict[str, str]) -> None: + def update_mobility(self, config: Dict[str, str]) -> None: if not self.mobility: raise CoreError(f"no mobility set to update for node({self.name})") self.mobility.update_config(config) - def updatemodel(self, config: dict[str, str]) -> None: + def updatemodel(self, config: Dict[str, str]) -> None: if not self.wireless_model: raise CoreError(f"no model set to update for node({self.name})") logger.debug( @@ -768,7 +759,7 @@ class WlanNode(CoreNetwork): for iface in self.get_ifaces(): iface.setposition() - def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Retrieve all link data. diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 30640fd8..8ab13f20 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -4,7 +4,7 @@ PhysicalNode class for including real systems in the emulated network. import logging from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional, Tuple import netaddr @@ -52,7 +52,7 @@ class Rj45Node(CoreNodeBase): ) self.iface.transport_type = TransportType.RAW self.old_up: bool = False - self.old_addrs: list[tuple[str, Optional[str]]] = [] + self.old_addrs: List[Tuple[str, Optional[str]]] = [] def startup(self) -> None: """ @@ -159,7 +159,7 @@ class Rj45Node(CoreNodeBase): """ # TODO: save/restore the PROMISC flag self.old_up = False - self.old_addrs: list[tuple[str, Optional[str]]] = [] + self.old_addrs: List[Tuple[str, Optional[str]]] = [] localname = self.iface.localname output = self.net_client.address_show(localname) for line in output.split("\n"): @@ -171,10 +171,7 @@ class Rj45Node(CoreNodeBase): if "UP" in flags: self.old_up = True elif items[0] == "inet": - broadcast = None - if items[2] == "brd": - broadcast = items[3] - self.old_addrs.append((items[1], broadcast)) + self.old_addrs.append((items[1], items[3])) elif items[0] == "inet6": if items[1][:4] == "fe80": continue diff --git a/daemon/core/nodes/podman.py b/daemon/core/nodes/podman.py deleted file mode 100644 index 00ef24fc..00000000 --- a/daemon/core/nodes/podman.py +++ /dev/null @@ -1,271 +0,0 @@ -import json -import logging -import shlex -from dataclasses import dataclass, field -from pathlib import Path -from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING - -from core.emulator.distributed import DistributedServer -from core.errors import CoreCommandError, CoreError -from core.executables import BASH -from core.nodes.base import CoreNode, CoreNodeOptions - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from core.emulator.session import Session - -PODMAN: str = "podman" - - -@dataclass -class PodmanOptions(CoreNodeOptions): - image: str = "ubuntu" - """image used when creating container""" - binds: list[tuple[str, str]] = field(default_factory=list) - """bind mount source and destinations to setup within container""" - volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list) - """ - volume mount source, destination, unique, delete to setup within container - - unique is True for node unique volume naming - delete is True for deleting volume mount during shutdown - """ - - -@dataclass -class VolumeMount: - src: str - """volume mount name""" - dst: str - """volume mount destination directory""" - unique: bool = True - """True to create a node unique prefixed name for this volume""" - delete: bool = True - """True to delete the volume during shutdown""" - path: str = None - """path to the volume on the host""" - - -class PodmanNode(CoreNode): - """ - Provides logic for creating a Podman based node. - """ - - def __init__( - self, - session: "Session", - _id: int = None, - name: str = None, - server: DistributedServer = None, - options: PodmanOptions = None, - ) -> None: - """ - Create a PodmanNode instance. - - :param session: core session instance - :param _id: node id - :param name: node name - :param server: remote server node - will run on, default is None for localhost - :param options: options for creating node - """ - options = options or PodmanOptions() - super().__init__(session, _id, name, server, options) - self.image: str = options.image - self.binds: list[tuple[str, str]] = options.binds - self.volumes: dict[str, VolumeMount] = {} - for src, dst, unique, delete in options.volumes: - src_name = self._unique_name(src) if unique else src - self.volumes[src] = VolumeMount(src_name, dst, unique, delete) - - @classmethod - def create_options(cls) -> PodmanOptions: - """ - Return default creation options, which can be used during node creation. - - :return: podman options - """ - return PodmanOptions() - - def create_cmd(self, args: str, shell: bool = False) -> str: - """ - Create command used to run commands within the context of a node. - - :param args: command arguments - :param shell: True to run shell like, False otherwise - :return: node command - """ - if shell: - args = f"{BASH} -c {shlex.quote(args)}" - return f"{PODMAN} exec {self.name} {args}" - - def _unique_name(self, name: str) -> str: - """ - Creates a session/node unique prefixed name for the provided input. - - :param name: name to make unique - :return: unique session/node prefixed name - """ - return f"{self.session.id}.{self.id}.{name}" - - def alive(self) -> bool: - """ - Check if the node is alive. - - :return: True if node is alive, False otherwise - """ - try: - running = self.host_cmd( - f"{PODMAN} inspect -f '{{{{.State.Running}}}}' {self.name}" - ) - return json.loads(running) - except CoreCommandError: - return False - - def startup(self) -> None: - """ - Create a podman container instance for the specified image. - - :return: nothing - """ - with self.lock: - if self.up: - raise CoreError(f"starting node({self.name}) that is already up") - # create node directory - self.makenodedir() - # setup commands for creating bind/volume mounts - binds = "" - for src, dst in self.binds: - binds += f"--mount type=bind,source={src},target={dst} " - volumes = "" - for volume in self.volumes.values(): - volumes += ( - f"--mount type=volume," f"source={volume.src},target={volume.dst} " - ) - # normalize hostname - hostname = self.name.replace("_", "-") - # create container and retrieve the created containers PID - self.host_cmd( - f"{PODMAN} run -td --init --net=none --hostname {hostname} " - f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 " - f"{binds} {volumes} " - f"--privileged {self.image} tail -f /dev/null" - ) - # retrieve pid and process environment for use in nsenter commands - self.pid = self.host_cmd( - f"{PODMAN} inspect -f '{{{{.State.Pid}}}}' {self.name}" - ) - # setup symlinks for bind and volume mounts within - for src, dst in self.binds: - link_path = self.host_path(Path(dst), True) - self.host_cmd(f"ln -s {src} {link_path}") - for volume in self.volumes.values(): - volume.path = self.host_cmd( - f"{PODMAN} volume inspect -f '{{{{.Mountpoint}}}}' {volume.src}" - ) - link_path = self.host_path(Path(volume.dst), True) - self.host_cmd(f"ln -s {volume.path} {link_path}") - logger.debug("node(%s) pid: %s", self.name, self.pid) - self.up = True - - def shutdown(self) -> None: - """ - Shutdown logic. - - :return: nothing - """ - # nothing to do if node is not up - if not self.up: - return - with self.lock: - self.ifaces.clear() - self.host_cmd(f"{PODMAN} rm -f {self.name}") - for volume in self.volumes.values(): - if volume.delete: - self.host_cmd(f"{PODMAN} volume rm {volume.src}") - self.up = False - - def termcmdstring(self, sh: str = "/bin/sh") -> str: - """ - Create a terminal command string. - - :param sh: shell to execute command in - :return: str - """ - terminal = f"{PODMAN} exec -it {self.name} {sh}" - if self.server is None: - return terminal - else: - return f"ssh -X -f {self.server.host} xterm -e {terminal}" - - def create_dir(self, dir_path: Path) -> None: - """ - Create a private directory. - - :param dir_path: path to create - :return: nothing - """ - logger.debug("creating node dir: %s", dir_path) - self.cmd(f"mkdir -p {dir_path}") - - def mount(self, src_path: str, target_path: str) -> None: - """ - Create and mount a directory. - - :param src_path: source directory to mount - :param target_path: target directory to create - :return: nothing - :raises CoreCommandError: when a non-zero exit status occurs - """ - logger.debug("mounting source(%s) target(%s)", src_path, target_path) - raise Exception("not supported") - - def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: - """ - Create a node file with a given mode. - - :param file_path: name of file to create - :param contents: contents of file - :param mode: mode for file - :return: nothing - """ - logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) - temp = NamedTemporaryFile(delete=False) - temp.write(contents.encode()) - temp.close() - temp_path = Path(temp.name) - directory = file_path.parent - if str(directory) != ".": - self.cmd(f"mkdir -m {0o755:o} -p {directory}") - if self.server is not None: - self.server.remote_put(temp_path, temp_path) - self.host_cmd(f"{PODMAN} cp {temp_path} {self.name}:{file_path}") - self.cmd(f"chmod {mode:o} {file_path}") - if self.server is not None: - self.host_cmd(f"rm -f {temp_path}") - temp_path.unlink() - - def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: - """ - Copy a file to a node, following symlinks and preserving metadata. - Change file mode if specified. - - :param dst_path: file name to copy file to - :param src_path: file to copy - :param mode: mode to copy to - :return: nothing - """ - logger.info( - "node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0 - ) - self.cmd(f"mkdir -p {dst_path.parent}") - if self.server: - temp = NamedTemporaryFile(delete=False) - temp_path = Path(temp.name) - src_path = temp_path - self.server.remote_put(src_path, temp_path) - self.host_cmd(f"{PODMAN} cp {src_path} {self.name}:{dst_path}") - if mode is not None: - self.cmd(f"chmod {mode:o} {dst_path}") diff --git a/daemon/core/nodes/wireless.py b/daemon/core/nodes/wireless.py index 51a98917..ef37db35 100644 --- a/daemon/core/nodes/wireless.py +++ b/daemon/core/nodes/wireless.py @@ -7,7 +7,7 @@ import logging import math import secrets from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Set, Tuple from core.config import ConfigBool, ConfigFloat, ConfigInt, Configuration from core.emulator.data import LinkData, LinkOptions @@ -41,7 +41,7 @@ KEY_LOSS: str = "loss" def calc_distance( - point1: tuple[float, float, float], point2: tuple[float, float, float] + point1: Tuple[float, float, float], point2: Tuple[float, float, float] ) -> float: a = point1[0] - point2[0] b = point1[1] - point2[1] @@ -51,7 +51,7 @@ def calc_distance( return math.hypot(math.hypot(a, b), c) -def get_key(node1_id: int, node2_id: int) -> tuple[int, int]: +def get_key(node1_id: int, node2_id: int) -> Tuple[int, int]: return (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id) @@ -65,7 +65,7 @@ class WirelessLink: class WirelessNode(CoreNetworkBase): - options: list[Configuration] = [ + options: List[Configuration] = [ ConfigBool( id=KEY_ENABLED, default="1" if CONFIG_ENABLED else "0", label="Enabled?" ), @@ -87,7 +87,7 @@ class WirelessNode(CoreNetworkBase): ), ConfigFloat(id=KEY_LOSS, default=str(CONFIG_LOSS), label="Loss Initial"), ] - devices: set[str] = set() + devices: Set[str] = set() @classmethod def add_device(cls) -> str: @@ -111,8 +111,8 @@ class WirelessNode(CoreNetworkBase): options: NodeOptions = None, ): super().__init__(session, _id, name, server, options) - self.bridges: dict[int, tuple[CoreInterface, str]] = {} - self.links: dict[tuple[int, int], WirelessLink] = {} + self.bridges: Dict[int, Tuple[CoreInterface, str]] = {} + self.links: Dict[Tuple[int, int], WirelessLink] = {} self.position_enabled: bool = CONFIG_ENABLED self.bandwidth: int = CONFIG_BANDWIDTH self.delay: int = CONFIG_DELAY @@ -321,7 +321,7 @@ class WirelessNode(CoreNetworkBase): def adopt_iface(self, iface: CoreInterface, name: str) -> None: raise CoreError(f"{type(self)} does not support adopt interface") - def get_config(self) -> dict[str, Configuration]: + def get_config(self) -> Dict[str, Configuration]: config = {x.id: x for x in copy.copy(self.options)} config[KEY_ENABLED].default = "1" if self.position_enabled else "0" config[KEY_RANGE].default = str(self.max_range) @@ -333,7 +333,7 @@ class WirelessNode(CoreNetworkBase): config[KEY_JITTER].default = str(self.jitter) return config - def set_config(self, config: dict[str, str]) -> None: + def set_config(self, config: Dict[str, str]) -> None: logger.info("wireless config: %s", config) self.position_enabled = config[KEY_ENABLED] == "1" self.max_range = float(config[KEY_RANGE]) diff --git a/daemon/core/player.py b/daemon/core/player.py index d06e7b97..6ba0d602 100644 --- a/daemon/core/player.py +++ b/daemon/core/player.py @@ -5,7 +5,7 @@ import logging import sched from pathlib import Path from threading import Thread -from typing import IO, Callable, Optional +from typing import IO, Callable, Dict, Optional import grpc @@ -230,7 +230,7 @@ class CorePlayer: self.node_streamer: Optional[MoveNodesStreamer] = None self.node_streamer_thread: Optional[Thread] = None self.scheduler: sched.scheduler = sched.scheduler() - self.handlers: dict[PlayerEvents, Callable] = { + self.handlers: Dict[PlayerEvents, Callable] = { PlayerEvents.XY: self.handle_xy, PlayerEvents.GEO: self.handle_geo, PlayerEvents.CMD: self.handle_cmd, diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index f963c817..48a6cdf0 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -5,7 +5,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from urllib.parse import urlparse from core.constants import CORE_CONF_DIR @@ -28,9 +28,9 @@ CORE_LAYER: str = "CORE" NODE_LAYER: str = "CORE::Nodes" LINK_LAYER: str = "CORE::Links" WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired" -CORE_LAYERS: list[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER] +CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER] DEFAULT_LINK_COLOR: str = "red" -NODE_TYPES: dict[type[NodeBase], str] = { +NODE_TYPES: Dict[Type[NodeBase], str] = { HubNode: "hub", SwitchNode: "lanswitch", TunnelNode: "tunnel", @@ -63,7 +63,7 @@ class Sdt: # default altitude (in meters) for flyto view DEFAULT_ALT: int = 2500 # TODO: read in user"s nodes.conf here; below are default node types from the GUI - DEFAULT_SPRITES: dict[str, str] = [ + DEFAULT_SPRITES: Dict[str, str] = [ ("router", "router.png"), ("host", "host.png"), ("PC", "pc.png"), @@ -88,9 +88,9 @@ class Sdt: self.sock: Optional[socket.socket] = None self.connected: bool = False self.url: str = self.DEFAULT_SDT_URL - self.address: Optional[tuple[Optional[str], Optional[int]]] = None + self.address: Optional[Tuple[Optional[str], Optional[int]]] = None self.protocol: Optional[str] = None - self.network_layers: set[str] = set() + self.network_layers: Set[str] = set() self.session.node_handlers.append(self.handle_node_update) self.session.link_handlers.append(self.handle_link_update) @@ -138,7 +138,7 @@ class Sdt: else: # Default to tcp self.sock = socket.create_connection(self.address, 5) - except OSError: + except IOError: logger.exception("SDT socket connect error") return False @@ -176,7 +176,7 @@ class Sdt: if self.sock: try: self.sock.close() - except OSError: + except IOError: logger.error("error closing socket") finally: self.sock = None @@ -212,7 +212,7 @@ class Sdt: logger.debug("sdt cmd: %s", cmd) self.sock.sendall(cmd) return True - except OSError: + except IOError: logger.exception("SDT connection error") self.sock = None self.connected = False diff --git a/daemon/core/scripts/cleanup.py b/daemon/core/scripts/cleanup.py index 1ab4647e..080cd62b 100755 --- a/daemon/core/scripts/cleanup.py +++ b/daemon/core/scripts/cleanup.py @@ -61,19 +61,17 @@ def cleanup_sessions() -> None: def cleanup_interfaces() -> None: print("cleaning up devices") - output = subprocess.check_output("ip -br link show", shell=True) + output = subprocess.check_output("ip -o -br link show", shell=True) lines = output.decode().strip().split("\n") for line in lines: values = line.split() name = values[0] if ( name.startswith("veth") - or name.startswith("beth") or name.startswith("gt.") or name.startswith("b.") or name.startswith("ctrl") ): - name = name.split("@")[0] result = subprocess.call(f"ip link delete {name}", shell=True) if result: print(f"failed to remove {name}") diff --git a/daemon/core/scripts/cli.py b/daemon/core/scripts/cli.py index 760dbad7..31ad086e 100755 --- a/daemon/core/scripts/cli.py +++ b/daemon/core/scripts/cli.py @@ -8,7 +8,7 @@ from argparse import ( ) from functools import wraps from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, Optional, Tuple import grpc import netaddr @@ -30,7 +30,7 @@ from core.api.grpc.wrappers import ( NODE_TYPES = [x.name for x in NodeType if x != NodeType.PEER_TO_PEER] -def protobuf_to_json(message: Any) -> dict[str, Any]: +def protobuf_to_json(message: Any) -> Dict[str, Any]: return MessageToDict( message, including_default_value_fields=True, preserving_proto_field_name=True ) @@ -82,7 +82,7 @@ def ip6_type(value: str) -> IPNetwork: raise ArgumentTypeError(f"invalid ip6 address: {value}") -def position_type(value: str) -> tuple[float, float]: +def position_type(value: str) -> Tuple[float, float]: error = "invalid position, must be in the format: float,float" try: values = [float(x) for x in value.split(",")] @@ -94,7 +94,7 @@ def position_type(value: str) -> tuple[float, float]: return x, y -def geo_type(value: str) -> tuple[float, float, float]: +def geo_type(value: str) -> Tuple[float, float, float]: error = "invalid geo, must be in the format: float,float,float" try: values = [float(x) for x in value.split(",")] diff --git a/daemon/core/scripts/routemonitor.py b/daemon/core/scripts/routemonitor.py index 42fbf3a9..2ebfdfad 100755 --- a/daemon/core/scripts/routemonitor.py +++ b/daemon/core/scripts/routemonitor.py @@ -9,6 +9,7 @@ from argparse import ArgumentDefaultsHelpFormatter from functools import cmp_to_key from queue import Queue from threading import Thread +from typing import Dict, Tuple import grpc @@ -30,7 +31,7 @@ class RouteEnum(enum.Enum): class SdtClient: - def __init__(self, address: tuple[str, int]) -> None: + def __init__(self, address: Tuple[str, int]) -> None: self.sock = socket.create_connection(address) self.links = [] self.send(f'layer "{ROUTE_LAYER}"') @@ -84,7 +85,7 @@ class RouterMonitor: self.sdt = SdtClient((sdt_host, sdt_port)) self.nodes = self.get_nodes() - def get_nodes(self) -> dict[int, str]: + def get_nodes(self) -> Dict[int, str]: with self.core.context_connect(): if self.session is None: self.session = self.get_session() @@ -145,7 +146,7 @@ class RouterMonitor: self.manage_routes() self.route_time = time.monotonic() - def route_sort(self, x: tuple[str, int], y: tuple[str, int]) -> int: + def route_sort(self, x: Tuple[str, int], y: Tuple[str, int]) -> int: x_node = x[0] y_node = y[0] if x_node == self.src_id: diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index c2ecc4dc..ffb177f3 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,7 +1,7 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ -from typing import Optional +from typing import Optional, Tuple from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -14,12 +14,12 @@ class Bird(CoreService): name: str = "bird" group: str = "BIRD" - executables: tuple[str, ...] = ("bird",) - dirs: tuple[str, ...] = ("/etc/bird",) - configs: tuple[str, ...] = ("/etc/bird/bird.conf",) - startup: tuple[str, ...] = (f"bird -c {configs[0]}",) - shutdown: tuple[str, ...] = ("killall bird",) - validate: tuple[str, ...] = ("pidof bird",) + executables: Tuple[str, ...] = ("bird",) + dirs: Tuple[str, ...] = ("/etc/bird",) + configs: Tuple[str, ...] = ("/etc/bird/bird.conf",) + startup: Tuple[str, ...] = ("bird -c %s" % (configs[0]),) + shutdown: Tuple[str, ...] = ("killall bird",) + validate: Tuple[str, ...] = ("pidof bird",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -48,30 +48,33 @@ class Bird(CoreService): Returns configuration file text. Other services that depend on bird will have hooks that are invoked here. """ - cfg = f"""\ + cfg = """\ /* Main configuration file for BIRD. This is ony a template, * you will *need* to customize it according to your needs * Beware that only double quotes \'"\' are valid. No singles. */ -log "/var/log/{cls.name}.log" all; +log "/var/log/%s.log" all; #debug protocols all; #debug commands 2; -router id {cls.router_id(node)}; # Mandatory for IPv6, may be automatic for IPv4 +router id %s; # Mandatory for IPv6, may be automatic for IPv4 -protocol kernel {{ +protocol kernel { persist; # Don\'t remove routes on BIRD shutdown scan time 200; # Scan kernel routing table every 200 seconds export all; import all; -}} +} -protocol device {{ +protocol device { scan time 10; # Scan interfaces every 10 seconds -}} +} -""" +""" % ( + cls.name, + cls.router_id(node), + ) # generate protocol specific configurations for s in node.services: @@ -91,8 +94,8 @@ class BirdService(CoreService): name: Optional[str] = None group: str = "BIRD" - executables: tuple[str, ...] = ("bird",) - dependencies: tuple[str, ...] = ("bird",) + executables: Tuple[str, ...] = ("bird",) + dependencies: Tuple[str, ...] = ("bird",) meta: str = "The config file for this service can be found in the bird service." @classmethod @@ -108,7 +111,7 @@ class BirdService(CoreService): """ cfg = "" for iface in node.get_ifaces(control=False): - cfg += f' interface "{iface.name}";\n' + cfg += ' interface "%s";\n' % iface.name return cfg diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 0eee980e..6e52b5d6 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -11,9 +11,18 @@ import enum import logging import pkgutil import time -from collections.abc import Iterable from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union +from typing import ( + TYPE_CHECKING, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + Union, +) from core import services as core_services from core import utils @@ -32,7 +41,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.emulator.session import Session - CoreServiceType = Union["CoreService", type["CoreService"]] + CoreServiceType = Union["CoreService", Type["CoreService"]] class ServiceMode(enum.Enum): @@ -48,25 +57,25 @@ class ServiceDependencies: provided. """ - def __init__(self, services: list["CoreServiceType"]) -> None: - self.visited: set[str] = set() - self.services: dict[str, "CoreServiceType"] = {} - self.paths: dict[str, list["CoreServiceType"]] = {} - self.boot_paths: list[list["CoreServiceType"]] = [] - roots = {x.name for x in services} + def __init__(self, services: List["CoreServiceType"]) -> None: + self.visited: Set[str] = set() + self.services: Dict[str, "CoreServiceType"] = {} + self.paths: Dict[str, List["CoreServiceType"]] = {} + self.boot_paths: List[List["CoreServiceType"]] = [] + roots = set([x.name for x in services]) for service in services: self.services[service.name] = service roots -= set(service.dependencies) - self.roots: list["CoreServiceType"] = [x for x in services if x.name in roots] + self.roots: List["CoreServiceType"] = [x for x in services if x.name in roots] if services and not self.roots: raise ValueError("circular dependency is present") def _search( self, service: "CoreServiceType", - visiting: set[str] = None, - path: list[str] = None, - ) -> list["CoreServiceType"]: + visiting: Set[str] = None, + path: List[str] = None, + ) -> List["CoreServiceType"]: if service.name in self.visited: return self.paths[service.name] self.visited.add(service.name) @@ -94,7 +103,7 @@ class ServiceDependencies: self.paths[service.name] = path return path - def boot_order(self) -> list[list["CoreServiceType"]]: + def boot_order(self) -> List[List["CoreServiceType"]]: for service in self.roots: self._search(service) return self.boot_paths @@ -105,10 +114,10 @@ class ServiceManager: Manages services available for CORE nodes to use. """ - services: dict[str, type["CoreService"]] = {} + services: Dict[str, Type["CoreService"]] = {} @classmethod - def add(cls, service: type["CoreService"]) -> None: + def add(cls, service: Type["CoreService"]) -> None: """ Add a service to manager. @@ -141,7 +150,7 @@ class ServiceManager: cls.services[name] = service @classmethod - def get(cls, name: str) -> type["CoreService"]: + def get(cls, name: str) -> Type["CoreService"]: """ Retrieve a service from the manager. @@ -154,7 +163,7 @@ class ServiceManager: return service @classmethod - def add_services(cls, path: Path) -> list[str]: + def add_services(cls, path: Path) -> List[str]: """ Method for retrieving all CoreServices from a given path. @@ -174,7 +183,7 @@ class ServiceManager: return service_errors @classmethod - def load_locals(cls) -> list[str]: + def load_locals(cls) -> List[str]: errors = [] for module_info in pkgutil.walk_packages( core_services.__path__, f"{core_services.__name__}." @@ -209,7 +218,7 @@ class CoreServices: """ self.session: "Session" = session # dict of default services tuples, key is node type - self.default_services: dict[str, list[str]] = { + self.default_services: Dict[str, List[str]] = { "mdr": ["zebra", "OSPFv3MDR", "IPForward"], "PC": ["DefaultRoute"], "prouter": [], @@ -217,7 +226,7 @@ class CoreServices: "host": ["DefaultRoute", "SSH"], } # dict of node ids to dict of custom services by name - self.custom_services: dict[int, dict[str, "CoreService"]] = {} + self.custom_services: Dict[int, Dict[str, "CoreService"]] = {} def reset(self) -> None: """ @@ -264,7 +273,7 @@ class CoreServices: node_services[service.name] = service def add_services( - self, node: CoreNode, model: str, services: list[str] = None + self, node: CoreNode, model: str, services: List[str] = None ) -> None: """ Add services to a node. @@ -289,7 +298,7 @@ class CoreServices: continue node.services.append(service) - def all_configs(self) -> list[tuple[int, "CoreService"]]: + def all_configs(self) -> List[Tuple[int, "CoreService"]]: """ Return (node_id, service) tuples for all stored configs. Used when reconnecting to a session or opening XML. @@ -304,7 +313,7 @@ class CoreServices: configs.append((node_id, service)) return configs - def all_files(self, service: "CoreService") -> list[tuple[str, str]]: + def all_files(self, service: "CoreService") -> List[Tuple[str, str]]: """ Return all customized files stored with a service. Used when reconnecting to a session or opening XML. @@ -340,7 +349,7 @@ class CoreServices: if exceptions: raise CoreServiceBootError(*exceptions) - def _boot_service_path(self, node: CoreNode, boot_path: list["CoreServiceType"]): + def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]): logger.info( "booting node(%s) services: %s", node.name, @@ -391,7 +400,7 @@ class CoreServices: status = self.startup_service(node, service, wait) if status: raise CoreServiceBootError( - f"node({node.name}) service({service.name}) error during startup" + "node(%s) service(%s) error during startup" % (node.name, service.name) ) # blocking mode is finished @@ -416,7 +425,7 @@ class CoreServices: if status: raise CoreServiceBootError( - f"node({node.name}) service({service.name}) failed validation" + "node(%s) service(%s) failed validation" % (node.name, service.name) ) def copy_service_file(self, node: CoreNode, file_path: Path, cfg: str) -> bool: @@ -531,11 +540,11 @@ class CoreServices: # get the file data data = service.config_data.get(filename) if data is None: - data = service.generate_config(node, filename) + data = "%s" % service.generate_config(node, filename) else: - data = data + data = "%s" % data - filetypestr = f"service:{service.name}" + filetypestr = "service:%s" % service.name return FileData( message_type=MessageFlags.ADD, node=node.id, @@ -627,7 +636,7 @@ class CoreServices: try: if self.copy_service_file(node, file_path, cfg): continue - except OSError: + except IOError: logger.exception("error copying service file: %s", file_name) continue else: @@ -665,31 +674,31 @@ class CoreService: name: Optional[str] = None # executables that must exist for service to run - executables: tuple[str, ...] = () + executables: Tuple[str, ...] = () # sets service requirements that must be started prior to this service starting - dependencies: tuple[str, ...] = () + dependencies: Tuple[str, ...] = () # group string allows grouping services together group: Optional[str] = None # private, per-node directories required by this service - dirs: tuple[str, ...] = () + dirs: Tuple[str, ...] = () # config files written by this service - configs: tuple[str, ...] = () + configs: Tuple[str, ...] = () # config file data - config_data: dict[str, str] = {} + config_data: Dict[str, str] = {} # list of startup commands - startup: tuple[str, ...] = () + startup: Tuple[str, ...] = () # list of shutdown commands - shutdown: tuple[str, ...] = () + shutdown: Tuple[str, ...] = () # list of validate commands - validate: tuple[str, ...] = () + validate: Tuple[str, ...] = () # validation mode, used to determine startup success validation_mode: ServiceMode = ServiceMode.NON_BLOCKING @@ -714,7 +723,7 @@ class CoreService: configuration is used to override their default parameters. """ self.custom: bool = True - self.config_data: dict[str, str] = self.__class__.config_data.copy() + self.config_data: Dict[str, str] = self.__class__.config_data.copy() @classmethod def on_load(cls) -> None: @@ -733,7 +742,7 @@ class CoreService: return cls.configs @classmethod - def generate_config(cls, node: CoreNode, filename: str) -> str: + def generate_config(cls, node: CoreNode, filename: str) -> None: """ Generate configuration file given a node object. The filename is provided to allow for multiple config files. @@ -742,7 +751,7 @@ class CoreService: :param node: node to generate config for :param filename: file name to generate config for - :return: generated config + :return: nothing """ raise NotImplementedError diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index 43cd9af4..4fd78ec1 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -1,3 +1,5 @@ +from typing import Tuple + from core.emane.nodes import EmaneNet from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -7,14 +9,14 @@ from core.xml import emanexml class EmaneTransportService(CoreService): name: str = "transportd" group: str = "EMANE" - executables: tuple[str, ...] = ("emanetransportd", "emanegentransportxml") - dependencies: tuple[str, ...] = () - dirs: tuple[str, ...] = () - configs: tuple[str, ...] = ("emanetransport.sh",) - startup: tuple[str, ...] = (f"bash {configs[0]}",) - validate: tuple[str, ...] = (f"pidof {executables[0]}",) + executables: Tuple[str, ...] = ("emanetransportd", "emanegentransportxml") + dependencies: Tuple[str, ...] = () + dirs: Tuple[str, ...] = () + configs: Tuple[str, ...] = ("emanetransport.sh",) + startup: Tuple[str, ...] = (f"bash {configs[0]}",) + validate: Tuple[str, ...] = (f"pidof {executables[0]}",) validation_timer: float = 0.5 - shutdown: tuple[str, ...] = (f"killall {executables[0]}",) + shutdown: Tuple[str, ...] = (f"killall {executables[0]}",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 28756c19..5fbacf42 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -2,7 +2,7 @@ frr.py: defines routing services provided by FRRouting. Assumes installation of FRR via https://deb.frrouting.org/ """ -from typing import Optional +from typing import Optional, Tuple import netaddr @@ -30,16 +30,16 @@ def is_wireless(node: NodeBase) -> bool: class FRRZebra(CoreService): name: str = "FRRzebra" group: str = "FRR" - dirs: tuple[str, ...] = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") - configs: tuple[str, ...] = ( + dirs: Tuple[str, ...] = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") + configs: Tuple[str, ...] = ( "/usr/local/etc/frr/frr.conf", "frrboot.sh", "/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/daemons", ) - startup: tuple[str, ...] = ("bash frrboot.sh zebra",) - shutdown: tuple[str, ...] = ("killall zebra",) - validate: tuple[str, ...] = ("pidof zebra",) + startup: Tuple[str, ...] = ("bash frrboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -75,7 +75,7 @@ class FRRZebra(CoreService): # we could verify here that filename == frr.conf cfg = "" for iface in node.get_ifaces(): - cfg += f"interface {iface.name}\n" + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons if iface.control: cfg += " " @@ -127,11 +127,11 @@ class FRRZebra(CoreService): """ address = str(ip.ip) if netaddr.valid_ipv4(address): - return f"ip address {ip}" + return "ip address %s" % ip elif netaddr.valid_ipv6(address): - return f"ipv6 address {ip}" + return "ipv6 address %s" % ip else: - raise ValueError(f"invalid address: {ip}") + raise ValueError("invalid address: %s", ip) @classmethod def generate_frr_boot(cls, node: CoreNode) -> str: @@ -142,19 +142,18 @@ class FRRZebra(CoreService): "frr_bin_search", '"/usr/local/bin /usr/bin /usr/lib/frr"' ) frr_sbin_search = node.session.options.get( - "frr_sbin_search", - '"/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr"', + "frr_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/frr"' ) - cfg = f"""\ + cfg = """\ #!/bin/sh # auto-generated by zebra service (frr.py) -FRR_CONF={cls.configs[0]} -FRR_SBIN_SEARCH={frr_sbin_search} -FRR_BIN_SEARCH={frr_bin_search} -FRR_STATE_DIR={FRR_STATE_DIR} +FRR_CONF=%s +FRR_SBIN_SEARCH=%s +FRR_BIN_SEARCH=%s +FRR_STATE_DIR=%s searchforprog() -{{ +{ prog=$1 searchpath=$@ ret= @@ -165,10 +164,10 @@ searchforprog() fi done echo $ret -}} +} confcheck() -{{ +{ CONF_DIR=`dirname $FRR_CONF` # if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then @@ -178,10 +177,10 @@ confcheck() if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf fi -}} +} bootdaemon() -{{ +{ FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH) if [ "z$FRR_SBIN_DIR" = "z" ]; then echo "ERROR: FRR's '$1' daemon not found in search path:" @@ -208,10 +207,10 @@ bootdaemon() echo "ERROR: FRR's '$1' daemon failed to start!:" return 1 fi -}} +} bootfrr() -{{ +{ FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH) if [ "z$FRR_BIN_DIR" = "z" ]; then echo "ERROR: FRR's 'vtysh' program not found in search path:" @@ -230,8 +229,8 @@ bootfrr() bootdaemon "staticd" fi for r in rip ripng ospf6 ospf bgp babel isis; do - if grep -q "^router \\<${{r}}\\>" $FRR_CONF; then - bootdaemon "${{r}}d" + if grep -q "^router \\<${r}\\>" $FRR_CONF; then + bootdaemon "${r}d" fi done @@ -240,7 +239,7 @@ bootfrr() fi $FRR_BIN_DIR/vtysh -b -}} +} if [ "$1" != "zebra" ]; then echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!" @@ -249,7 +248,12 @@ fi confcheck bootfrr -""" +""" % ( + cls.configs[0], + frr_sbin_search, + frr_bin_search, + FRR_STATE_DIR, + ) for iface in node.get_ifaces(): cfg += f"ip link set dev {iface.name} down\n" cfg += "sleep 1\n" @@ -333,7 +337,7 @@ class FrrService(CoreService): name: Optional[str] = None group: str = "FRR" - dependencies: tuple[str, ...] = ("FRRzebra",) + dependencies: Tuple[str, ...] = ("FRRzebra",) meta: str = "The config file for this service can be found in the Zebra service." ipv4_routing: bool = False ipv6_routing: bool = False @@ -384,8 +388,8 @@ class FRROspfv2(FrrService): """ name: str = "FRROSPFv2" - shutdown: tuple[str, ...] = ("killall ospfd",) - validate: tuple[str, ...] = ("pidof ospfd",) + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) ipv4_routing: bool = True @staticmethod @@ -420,7 +424,7 @@ class FRROspfv2(FrrService): def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" rtrid = cls.router_id(node) - cfg += f" router-id {rtrid}\n" + cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): for ip4 in iface.ip4s: @@ -454,8 +458,8 @@ class FRROspfv3(FrrService): """ name: str = "FRROSPFv3" - shutdown: tuple[str, ...] = ("killall ospf6d",) - validate: tuple[str, ...] = ("pidof ospf6d",) + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) ipv4_routing: bool = True ipv6_routing: bool = True @@ -482,7 +486,7 @@ class FRROspfv3(FrrService): """ minmtu = cls.min_mtu(iface) if minmtu < iface.mtu: - return f" ipv6 ospf6 ifmtu {minmtu:d}\n" + return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @@ -500,9 +504,9 @@ class FRROspfv3(FrrService): def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf6\n" rtrid = cls.router_id(node) - cfg += f" router-id {rtrid}\n" + cfg += " router-id %s\n" % rtrid for iface in node.get_ifaces(control=False): - cfg += f" interface {iface.name} area 0.0.0.0\n" + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @@ -519,8 +523,8 @@ class FRRBgp(FrrService): """ name: str = "FRRBGP" - shutdown: tuple[str, ...] = ("killall bgpd",) - validate: tuple[str, ...] = ("pidof bgpd",) + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) custom_needed: bool = True ipv4_routing: bool = True ipv6_routing: bool = True @@ -530,9 +534,9 @@ class FRRBgp(FrrService): cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" - cfg += f"router bgp {node.id}\n" + cfg += "router bgp %s\n" % node.id rtrid = cls.router_id(node) - cfg += f" bgp router-id {rtrid}\n" + cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" return cfg @@ -544,8 +548,8 @@ class FRRRip(FrrService): """ name: str = "FRRRIP" - shutdown: tuple[str, ...] = ("killall ripd",) - validate: tuple[str, ...] = ("pidof ripd",) + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) ipv4_routing: bool = True @classmethod @@ -567,8 +571,8 @@ class FRRRipng(FrrService): """ name: str = "FRRRIPNG" - shutdown: tuple[str, ...] = ("killall ripngd",) - validate: tuple[str, ...] = ("pidof ripngd",) + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) ipv6_routing: bool = True @classmethod @@ -591,15 +595,15 @@ class FRRBabel(FrrService): """ name: str = "FRRBabel" - shutdown: tuple[str, ...] = ("killall babeld",) - validate: tuple[str, ...] = ("pidof babeld",) + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) ipv6_routing: bool = True @classmethod def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router babel\n" for iface in node.get_ifaces(control=False): - cfg += f" network {iface.name}\n" + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute ipv4 connected\n" return cfg @@ -617,8 +621,8 @@ class FRRpimd(FrrService): """ name: str = "FRRpimd" - shutdown: tuple[str, ...] = ("killall pimd",) - validate: tuple[str, ...] = ("pidof pimd",) + shutdown: Tuple[str, ...] = ("killall pimd",) + validate: Tuple[str, ...] = ("pidof pimd",) ipv4_routing: bool = True @classmethod @@ -632,8 +636,8 @@ class FRRpimd(FrrService): cfg += "router igmp\n!\n" cfg += "router pim\n" cfg += " !ip pim rp-address 10.0.0.1\n" - cfg += f" ip pim bsr-candidate {ifname}\n" - cfg += f" ip pim rp-candidate {ifname}\n" + cfg += " ip pim bsr-candidate %s\n" % ifname + cfg += " ip pim rp-candidate %s\n" % ifname cfg += " !ip pim spt-threshold interval 10 bytes 80000\n" return cfg @@ -650,8 +654,8 @@ class FRRIsis(FrrService): """ name: str = "FRRISIS" - shutdown: tuple[str, ...] = ("killall isisd",) - validate: tuple[str, ...] = ("pidof isisd",) + shutdown: Tuple[str, ...] = ("killall isisd",) + validate: Tuple[str, ...] = ("pidof isisd",) ipv4_routing: bool = True ipv6_routing: bool = True @@ -668,7 +672,7 @@ class FRRIsis(FrrService): @classmethod def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router isis DEFAULT\n" - cfg += f" net 47.0001.0000.1900.{node.id:04x}.00\n" + cfg += " net 47.0001.0000.1900.%04x.00\n" % node.id cfg += " metric-style wide\n" cfg += " is-type level-2-only\n" cfg += "!\n" diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 32e19f60..9ef4e1d8 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -2,7 +2,7 @@ nrl.py: defines services provided by NRL protolib tools hosted here: http://www.nrl.navy.mil/itd/ncs/products """ -from typing import Optional +from typing import Optional, Tuple from core import utils from core.nodes.base import CoreNode @@ -33,29 +33,29 @@ class NrlService(CoreService): ip4 = iface.get_ip4() if ip4: return f"{ip4.ip}/{prefixlen}" - return f"0.0.0.0/{prefixlen}" + return "0.0.0.0/%s" % prefixlen class MgenSinkService(NrlService): name: str = "MGEN_Sink" - executables: tuple[str, ...] = ("mgen",) - configs: tuple[str, ...] = ("sink.mgen",) - startup: tuple[str, ...] = ("mgen input sink.mgen",) - validate: tuple[str, ...] = ("pidof mgen",) - shutdown: tuple[str, ...] = ("killall mgen",) + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("sink.mgen",) + startup: Tuple[str, ...] = ("mgen input sink.mgen",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "0.0 LISTEN UDP 5000\n" for iface in node.get_ifaces(): name = utils.sysctl_devname(iface.name) - cfg += f"0.0 Join 224.225.1.2 INTERFACE {name}\n" + cfg += "0.0 Join 224.225.1.2 INTERFACE %s\n" % name return cfg @classmethod - def get_startup(cls, node: CoreNode) -> tuple[str, ...]: + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: cmd = cls.startup[0] - cmd += f" output /tmp/mgen_{node.name}.log" + cmd += " output /tmp/mgen_%s.log" % node.name return (cmd,) @@ -65,23 +65,23 @@ class NrlNhdp(NrlService): """ name: str = "NHDP" - executables: tuple[str, ...] = ("nrlnhdp",) - startup: tuple[str, ...] = ("nrlnhdp",) - shutdown: tuple[str, ...] = ("killall nrlnhdp",) - validate: tuple[str, ...] = ("pidof nrlnhdp",) + executables: Tuple[str, ...] = ("nrlnhdp",) + startup: Tuple[str, ...] = ("nrlnhdp",) + shutdown: Tuple[str, ...] = ("killall nrlnhdp",) + validate: Tuple[str, ...] = ("pidof nrlnhdp",) @classmethod - def get_startup(cls, node: CoreNode) -> tuple[str, ...]: + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlnhdp.log" - cmd += f" -rpipe {node.name}_nhdp" + cmd += " -rpipe %s_nhdp" % node.name servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" - cmd += f" -smfClient {node.name}_smf" + cmd += " -smfClient %s_smf" % node.name ifaces = node.get_ifaces(control=False) if len(ifaces) > 0: iface_names = map(lambda x: x.name, ifaces) @@ -96,11 +96,11 @@ class NrlSmf(NrlService): """ name: str = "SMF" - executables: tuple[str, ...] = ("nrlsmf",) - startup: tuple[str, ...] = ("bash startsmf.sh",) - shutdown: tuple[str, ...] = ("killall nrlsmf",) - validate: tuple[str, ...] = ("pidof nrlsmf",) - configs: tuple[str, ...] = ("startsmf.sh",) + executables: Tuple[str, ...] = ("nrlsmf",) + startup: Tuple[str, ...] = ("bash startsmf.sh",) + shutdown: Tuple[str, ...] = ("killall nrlsmf",) + validate: Tuple[str, ...] = ("pidof nrlsmf",) + configs: Tuple[str, ...] = ("startsmf.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -112,7 +112,7 @@ class NrlSmf(NrlService): cfg = "#!/bin/sh\n" cfg += "# auto-generated by nrl.py:NrlSmf.generateconfig()\n" comments = "" - cmd = f"nrlsmf instance {node.name}_smf" + cmd = "nrlsmf instance %s_smf" % node.name servicenames = map(lambda x: x.name, node.services) ifaces = node.get_ifaces(control=False) @@ -142,13 +142,13 @@ class NrlOlsr(NrlService): """ name: str = "OLSR" - executables: tuple[str, ...] = ("nrlolsrd",) - startup: tuple[str, ...] = ("nrlolsrd",) - shutdown: tuple[str, ...] = ("killall nrlolsrd",) - validate: tuple[str, ...] = ("pidof nrlolsrd",) + executables: Tuple[str, ...] = ("nrlolsrd",) + startup: Tuple[str, ...] = ("nrlolsrd",) + shutdown: Tuple[str, ...] = ("killall nrlolsrd",) + validate: Tuple[str, ...] = ("pidof nrlolsrd",) @classmethod - def get_startup(cls, node: CoreNode) -> tuple[str, ...]: + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ @@ -157,13 +157,13 @@ class NrlOlsr(NrlService): ifaces = node.get_ifaces() if len(ifaces) > 0: iface = ifaces[0] - cmd += f" -i {iface.name}" + cmd += " -i %s" % iface.name cmd += " -l /var/log/nrlolsrd.log" - cmd += f" -rpipe {node.name}_olsr" + cmd += " -rpipe %s_olsr" % node.name servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames and "NHDP" not in servicenames: cmd += " -flooding s-mpr" - cmd += f" -smfClient {node.name}_smf" + cmd += " -smfClient %s_smf" % node.name if "zebra" in servicenames: cmd += " -z" return (cmd,) @@ -175,23 +175,23 @@ class NrlOlsrv2(NrlService): """ name: str = "OLSRv2" - executables: tuple[str, ...] = ("nrlolsrv2",) - startup: tuple[str, ...] = ("nrlolsrv2",) - shutdown: tuple[str, ...] = ("killall nrlolsrv2",) - validate: tuple[str, ...] = ("pidof nrlolsrv2",) + executables: Tuple[str, ...] = ("nrlolsrv2",) + startup: Tuple[str, ...] = ("nrlolsrv2",) + shutdown: Tuple[str, ...] = ("killall nrlolsrv2",) + validate: Tuple[str, ...] = ("pidof nrlolsrv2",) @classmethod - def get_startup(cls, node: CoreNode) -> tuple[str, ...]: + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlolsrv2.log" - cmd += f" -rpipe {node.name}_olsrv2" + cmd += " -rpipe %s_olsrv2" % node.name servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" - cmd += f" -smfClient {node.name}_smf" + cmd += " -smfClient %s_smf" % node.name cmd += " -p olsr" ifaces = node.get_ifaces(control=False) if len(ifaces) > 0: @@ -207,15 +207,15 @@ class OlsrOrg(NrlService): """ name: str = "OLSRORG" - executables: tuple[str, ...] = ("olsrd",) - configs: tuple[str, ...] = ("/etc/olsrd/olsrd.conf",) - dirs: tuple[str, ...] = ("/etc/olsrd",) - startup: tuple[str, ...] = ("olsrd",) - shutdown: tuple[str, ...] = ("killall olsrd",) - validate: tuple[str, ...] = ("pidof olsrd",) + executables: Tuple[str, ...] = ("olsrd",) + configs: Tuple[str, ...] = ("/etc/olsrd/olsrd.conf",) + dirs: Tuple[str, ...] = ("/etc/olsrd",) + startup: Tuple[str, ...] = ("olsrd",) + shutdown: Tuple[str, ...] = ("killall olsrd",) + validate: Tuple[str, ...] = ("pidof olsrd",) @classmethod - def get_startup(cls, node: CoreNode) -> tuple[str, ...]: + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ @@ -558,11 +558,11 @@ class MgenActor(NrlService): # a unique name is required, without spaces name: str = "MgenActor" group: str = "ProtoSvc" - executables: tuple[str, ...] = ("mgen",) - configs: tuple[str, ...] = ("start_mgen_actor.sh",) - startup: tuple[str, ...] = ("bash start_mgen_actor.sh",) - validate: tuple[str, ...] = ("pidof mgen",) - shutdown: tuple[str, ...] = ("killall mgen",) + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("start_mgen_actor.sh",) + startup: Tuple[str, ...] = ("bash start_mgen_actor.sh",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -574,7 +574,7 @@ class MgenActor(NrlService): cfg = "#!/bin/sh\n" cfg += "# auto-generated by nrl.py:MgenActor.generateconfig()\n" comments = "" - cmd = f"mgenBasicActor.py -n {node.name} -a 0.0.0.0" + cmd = "mgenBasicActor.py -n %s -a 0.0.0.0" % node.name ifaces = node.get_ifaces(control=False) if len(ifaces) == 0: return "" diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index b96a8eae..a2f06bec 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -1,7 +1,7 @@ """ quagga.py: defines routing services provided by Quagga. """ -from typing import Optional +from typing import Optional, Tuple import netaddr @@ -29,15 +29,15 @@ def is_wireless(node: NodeBase) -> bool: class Zebra(CoreService): name: str = "zebra" group: str = "Quagga" - dirs: tuple[str, ...] = ("/usr/local/etc/quagga", "/var/run/quagga") - configs: tuple[str, ...] = ( + dirs: Tuple[str, ...] = ("/usr/local/etc/quagga", "/var/run/quagga") + configs: Tuple[str, ...] = ( "/usr/local/etc/quagga/Quagga.conf", "quaggaboot.sh", "/usr/local/etc/quagga/vtysh.conf", ) - startup: tuple[str, ...] = ("bash quaggaboot.sh zebra",) - shutdown: tuple[str, ...] = ("killall zebra",) - validate: tuple[str, ...] = ("pidof zebra",) + startup: Tuple[str, ...] = ("bash quaggaboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -71,7 +71,7 @@ class Zebra(CoreService): # we could verify here that filename == Quagga.conf cfg = "" for iface in node.get_ifaces(): - cfg += f"interface {iface.name}\n" + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons if iface.control: cfg += " " @@ -123,11 +123,11 @@ class Zebra(CoreService): """ address = str(ip.ip) if netaddr.valid_ipv4(address): - return f"ip address {ip}" + return "ip address %s" % ip elif netaddr.valid_ipv6(address): - return f"ipv6 address {ip}" + return "ipv6 address %s" % ip else: - raise ValueError(f"invalid address: {ip}") + raise ValueError("invalid address: %s", ip) @classmethod def generate_quagga_boot(cls, node: CoreNode) -> str: @@ -140,16 +140,16 @@ class Zebra(CoreService): quagga_sbin_search = node.session.options.get( "quagga_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/quagga"' ) - return f"""\ + return """\ #!/bin/sh # auto-generated by zebra service (quagga.py) -QUAGGA_CONF={cls.configs[0]} -QUAGGA_SBIN_SEARCH={quagga_sbin_search} -QUAGGA_BIN_SEARCH={quagga_bin_search} -QUAGGA_STATE_DIR={QUAGGA_STATE_DIR} +QUAGGA_CONF=%s +QUAGGA_SBIN_SEARCH=%s +QUAGGA_BIN_SEARCH=%s +QUAGGA_STATE_DIR=%s searchforprog() -{{ +{ prog=$1 searchpath=$@ ret= @@ -160,10 +160,10 @@ searchforprog() fi done echo $ret -}} +} confcheck() -{{ +{ CONF_DIR=`dirname $QUAGGA_CONF` # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then @@ -173,10 +173,10 @@ confcheck() if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf fi -}} +} bootdaemon() -{{ +{ QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then echo "ERROR: Quagga's '$1' daemon not found in search path:" @@ -196,10 +196,10 @@ bootdaemon() echo "ERROR: Quagga's '$1' daemon failed to start!:" return 1 fi -}} +} bootquagga() -{{ +{ QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) if [ "z$QUAGGA_BIN_DIR" = "z" ]; then echo "ERROR: Quagga's 'vtysh' program not found in search path:" @@ -215,8 +215,8 @@ bootquagga() bootdaemon "zebra" for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \\<${{r}}\\>" $QUAGGA_CONF; then - bootdaemon "${{r}}d" + if grep -q "^router \\<${r}\\>" $QUAGGA_CONF; then + bootdaemon "${r}d" fi done @@ -225,7 +225,7 @@ bootquagga() fi $QUAGGA_BIN_DIR/vtysh -b -}} +} if [ "$1" != "zebra" ]; then echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" @@ -233,7 +233,12 @@ if [ "$1" != "zebra" ]; then fi confcheck bootquagga -""" +""" % ( + cls.configs[0], + quagga_sbin_search, + quagga_bin_search, + QUAGGA_STATE_DIR, + ) class QuaggaService(CoreService): @@ -244,7 +249,7 @@ class QuaggaService(CoreService): name: Optional[str] = None group: str = "Quagga" - dependencies: tuple[str, ...] = (Zebra.name,) + dependencies: Tuple[str, ...] = (Zebra.name,) meta: str = "The config file for this service can be found in the Zebra service." ipv4_routing: bool = False ipv6_routing: bool = False @@ -295,8 +300,8 @@ class Ospfv2(QuaggaService): """ name: str = "OSPFv2" - shutdown: tuple[str, ...] = ("killall ospfd",) - validate: tuple[str, ...] = ("pidof ospfd",) + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) ipv4_routing: bool = True @staticmethod @@ -331,7 +336,7 @@ class Ospfv2(QuaggaService): def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" rtrid = cls.router_id(node) - cfg += f" router-id {rtrid}\n" + cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): for ip4 in iface.ip4s: @@ -364,8 +369,8 @@ class Ospfv3(QuaggaService): """ name: str = "OSPFv3" - shutdown: tuple[str, ...] = ("killall ospf6d",) - validate: tuple[str, ...] = ("pidof ospf6d",) + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) ipv4_routing: bool = True ipv6_routing: bool = True @@ -392,7 +397,7 @@ class Ospfv3(QuaggaService): """ minmtu = cls.min_mtu(iface) if minmtu < iface.mtu: - return f" ipv6 ospf6 ifmtu {minmtu:d}\n" + return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @@ -411,9 +416,9 @@ class Ospfv3(QuaggaService): cfg = "router ospf6\n" rtrid = cls.router_id(node) cfg += " instance-id 65\n" - cfg += f" router-id {rtrid}\n" + cfg += " router-id %s\n" % rtrid for iface in node.get_ifaces(control=False): - cfg += f" interface {iface.name} area 0.0.0.0\n" + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @@ -461,8 +466,8 @@ class Bgp(QuaggaService): """ name: str = "BGP" - shutdown: tuple[str, ...] = ("killall bgpd",) - validate: tuple[str, ...] = ("pidof bgpd",) + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) custom_needed: bool = True ipv4_routing: bool = True ipv6_routing: bool = True @@ -472,9 +477,9 @@ class Bgp(QuaggaService): cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" - cfg += f"router bgp {node.id}\n" + cfg += "router bgp %s\n" % node.id rtrid = cls.router_id(node) - cfg += f" bgp router-id {rtrid}\n" + cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" return cfg @@ -486,8 +491,8 @@ class Rip(QuaggaService): """ name: str = "RIP" - shutdown: tuple[str, ...] = ("killall ripd",) - validate: tuple[str, ...] = ("pidof ripd",) + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) ipv4_routing: bool = True @classmethod @@ -509,8 +514,8 @@ class Ripng(QuaggaService): """ name: str = "RIPNG" - shutdown: tuple[str, ...] = ("killall ripngd",) - validate: tuple[str, ...] = ("pidof ripngd",) + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) ipv6_routing: bool = True @classmethod @@ -533,15 +538,15 @@ class Babel(QuaggaService): """ name: str = "Babel" - shutdown: tuple[str, ...] = ("killall babeld",) - validate: tuple[str, ...] = ("pidof babeld",) + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) ipv6_routing: bool = True @classmethod def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router babel\n" for iface in node.get_ifaces(control=False): - cfg += f" network {iface.name}\n" + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute connected\n" return cfg @@ -559,8 +564,8 @@ class Xpimd(QuaggaService): """ name: str = "Xpimd" - shutdown: tuple[str, ...] = ("killall xpimd",) - validate: tuple[str, ...] = ("pidof xpimd",) + shutdown: Tuple[str, ...] = ("killall xpimd",) + validate: Tuple[str, ...] = ("pidof xpimd",) ipv4_routing: bool = True @classmethod @@ -574,8 +579,8 @@ class Xpimd(QuaggaService): cfg += "router igmp\n!\n" cfg += "router pim\n" cfg += " !ip pim rp-address 10.0.0.1\n" - cfg += f" ip pim bsr-candidate {ifname}\n" - cfg += f" ip pim rp-candidate {ifname}\n" + cfg += " ip pim bsr-candidate %s\n" % ifname + cfg += " ip pim rp-candidate %s\n" % ifname cfg += " !ip pim spt-threshold interval 10 bytes 80000\n" return cfg diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index a31cf87d..e72b5138 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -3,6 +3,7 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. """ import re +from typing import Tuple from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -23,15 +24,15 @@ class SdnService(CoreService): class OvsService(SdnService): name: str = "OvsService" group: str = "SDN" - executables: tuple[str, ...] = ("ovs-ofctl", "ovs-vsctl") - dirs: tuple[str, ...] = ( + executables: Tuple[str, ...] = ("ovs-ofctl", "ovs-vsctl") + dirs: Tuple[str, ...] = ( "/etc/openvswitch", "/var/run/openvswitch", "/var/log/openvswitch", ) - configs: tuple[str, ...] = ("OvsService.sh",) - startup: tuple[str, ...] = ("bash OvsService.sh",) - shutdown: tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server") + configs: Tuple[str, ...] = ("OvsService.sh",) + startup: Tuple[str, ...] = ("bash OvsService.sh",) + shutdown: Tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server") @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -58,41 +59,39 @@ class OvsService(SdnService): # create virtual interfaces cfg += "## Create a veth pair to send the data to\n" - cfg += f"ip link add rtr{ifnum} type veth peer name sw{ifnum}\n" + cfg += "ip link add rtr%s type veth peer name sw%s\n" % (ifnum, ifnum) # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running for ip4 in iface.ip4s: - cfg += f"ip addr del {ip4.ip} dev {iface.name}\n" + cfg += "ip addr del %s dev %s\n" % (ip4.ip, iface.name) if has_zebra == 0: - cfg += f"ip addr add {ip4.ip} dev rtr{ifnum}\n" + cfg += "ip addr add %s dev rtr%s\n" % (ip4.ip, ifnum) for ip6 in iface.ip6s: - cfg += f"ip -6 addr del {ip6.ip} dev {iface.name}\n" + cfg += "ip -6 addr del %s dev %s\n" % (ip6.ip, iface.name) if has_zebra == 0: - cfg += f"ip -6 addr add {ip6.ip} dev rtr{ifnum}\n" + cfg += "ip -6 addr add %s dev rtr%s\n" % (ip6.ip, ifnum) # add interfaces to bridge - # Make port numbers explicit so they're easier to follow in - # reading the script + # Make port numbers explicit so they're easier to follow in reading the script cfg += "## Add the CORE interface to the switch\n" cfg += ( - f"ovs-vsctl add-port ovsbr0 eth{ifnum} -- " - f"set Interface eth{ifnum} ofport_request={portnum:d}\n" + "ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n" + % (ifnum, ifnum, portnum) ) cfg += "## And then add its sibling veth interface\n" cfg += ( - f"ovs-vsctl add-port ovsbr0 sw{ifnum} -- " - f"set Interface sw{ifnum} ofport_request={portnum + 1:d}\n" + "ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n" + % (ifnum, ifnum, portnum + 1) ) cfg += "## start them up so we can send/receive data\n" - cfg += f"ovs-ofctl mod-port ovsbr0 eth{ifnum} up\n" - cfg += f"ovs-ofctl mod-port ovsbr0 sw{ifnum} up\n" + cfg += "ovs-ofctl mod-port ovsbr0 eth%s up\n" % ifnum + cfg += "ovs-ofctl mod-port ovsbr0 sw%s up\n" % ifnum cfg += "## Bring up the lower part of the veth pair\n" - cfg += f"ip link set dev rtr{ifnum} up\n" + cfg += "ip link set dev rtr%s up\n" % ifnum portnum += 2 - # Add rule for default controller if there is one local - # (even if the controller is not local, it finds it) + # Add rule for default controller if there is one local (even if the controller is not local, it finds it) cfg += "\n## We assume there will be an SDN controller on the other end of this, \n" cfg += "## but it will still function if there's not\n" cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n" @@ -103,8 +102,14 @@ class OvsService(SdnService): portnum = 1 for iface in node.get_ifaces(control=False): cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n" - cfg += f"ovs-ofctl add-flow ovsbr0 priority=1000,in_port={portnum:d},action=output:{portnum + 1:d}\n" - cfg += f"ovs-ofctl add-flow ovsbr0 priority=1000,in_port={portnum + 1:d},action=output:{portnum:d}\n" + cfg += ( + "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" + % (portnum, portnum + 1) + ) + cfg += ( + "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" + % (portnum + 1, portnum) + ) portnum += 2 return cfg @@ -112,10 +117,10 @@ class OvsService(SdnService): class RyuService(SdnService): name: str = "ryuService" group: str = "SDN" - executables: tuple[str, ...] = ("ryu-manager",) - configs: tuple[str, ...] = ("ryuService.sh",) - startup: tuple[str, ...] = ("bash ryuService.sh",) - shutdown: tuple[str, ...] = ("killall ryu-manager",) + executables: Tuple[str, ...] = ("ryu-manager",) + configs: Tuple[str, ...] = ("ryuService.sh",) + startup: Tuple[str, ...] = ("bash ryuService.sh",) + shutdown: Tuple[str, ...] = ("killall ryu-manager",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index afd71a14..f53e8533 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -4,6 +4,7 @@ firewall) """ import logging +from typing import Tuple from core import constants from core.nodes.base import CoreNode @@ -16,10 +17,10 @@ logger = logging.getLogger(__name__) class VPNClient(CoreService): name: str = "VPNClient" group: str = "Security" - configs: tuple[str, ...] = ("vpnclient.sh",) - startup: tuple[str, ...] = ("bash vpnclient.sh",) - shutdown: tuple[str, ...] = ("killall openvpn",) - validate: tuple[str, ...] = ("pidof openvpn",) + configs: Tuple[str, ...] = ("vpnclient.sh",) + startup: Tuple[str, ...] = ("bash vpnclient.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) custom_needed: bool = True @classmethod @@ -31,9 +32,9 @@ class VPNClient(CoreService): cfg += "# custom VPN Client configuration for service (security.py)\n" fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNClient" try: - with open(fname) as f: + with open(fname, "r") as f: cfg += f.read() - except OSError: + except IOError: logger.exception( "error opening VPN client configuration template (%s)", fname ) @@ -43,10 +44,10 @@ class VPNClient(CoreService): class VPNServer(CoreService): name: str = "VPNServer" group: str = "Security" - configs: tuple[str, ...] = ("vpnserver.sh",) - startup: tuple[str, ...] = ("bash vpnserver.sh",) - shutdown: tuple[str, ...] = ("killall openvpn",) - validate: tuple[str, ...] = ("pidof openvpn",) + configs: Tuple[str, ...] = ("vpnserver.sh",) + startup: Tuple[str, ...] = ("bash vpnserver.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) custom_needed: bool = True @classmethod @@ -59,9 +60,9 @@ class VPNServer(CoreService): cfg += "# custom VPN Server Configuration for service (security.py)\n" fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNServer" try: - with open(fname) as f: + with open(fname, "r") as f: cfg += f.read() - except OSError: + except IOError: logger.exception( "Error opening VPN server configuration template (%s)", fname ) @@ -71,9 +72,9 @@ class VPNServer(CoreService): class IPsec(CoreService): name: str = "IPsec" group: str = "Security" - configs: tuple[str, ...] = ("ipsec.sh",) - startup: tuple[str, ...] = ("bash ipsec.sh",) - shutdown: tuple[str, ...] = ("killall racoon",) + configs: Tuple[str, ...] = ("ipsec.sh",) + startup: Tuple[str, ...] = ("bash ipsec.sh",) + shutdown: Tuple[str, ...] = ("killall racoon",) custom_needed: bool = True @classmethod @@ -87,9 +88,9 @@ class IPsec(CoreService): cfg += "(security.py)\n" fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleIPsec" try: - with open(fname) as f: + with open(fname, "r") as f: cfg += f.read() - except OSError: + except IOError: logger.exception("Error opening IPsec configuration template (%s)", fname) return cfg @@ -97,8 +98,8 @@ class IPsec(CoreService): class Firewall(CoreService): name: str = "Firewall" group: str = "Security" - configs: tuple[str, ...] = ("firewall.sh",) - startup: tuple[str, ...] = ("bash firewall.sh",) + configs: Tuple[str, ...] = ("firewall.sh",) + startup: Tuple[str, ...] = ("bash firewall.sh",) custom_needed: bool = True @classmethod @@ -110,9 +111,9 @@ class Firewall(CoreService): cfg += "# custom node firewall rules for service (security.py)\n" fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleFirewall" try: - with open(fname) as f: + with open(fname, "r") as f: cfg += f.read() - except OSError: + except IOError: logger.exception( "Error opening Firewall configuration template (%s)", fname ) @@ -126,9 +127,9 @@ class Nat(CoreService): name: str = "NAT" group: str = "Security" - executables: tuple[str, ...] = ("iptables",) - configs: tuple[str, ...] = ("nat.sh",) - startup: tuple[str, ...] = ("bash nat.sh",) + executables: Tuple[str, ...] = ("iptables",) + configs: Tuple[str, ...] = ("nat.sh",) + startup: Tuple[str, ...] = ("bash nat.sh",) custom_needed: bool = False @classmethod diff --git a/daemon/core/services/ucarp.py b/daemon/core/services/ucarp.py index c6f2256e..aa0d9a1a 100644 --- a/daemon/core/services/ucarp.py +++ b/daemon/core/services/ucarp.py @@ -1,6 +1,7 @@ """ ucarp.py: defines high-availability IP address controlled by ucarp """ +from typing import Tuple from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -11,16 +12,16 @@ UCARP_ETC = "/usr/local/etc/ucarp" class Ucarp(CoreService): name: str = "ucarp" group: str = "Utility" - dirs: tuple[str, ...] = (UCARP_ETC,) - configs: tuple[str, ...] = ( + dirs: Tuple[str, ...] = (UCARP_ETC,) + configs: Tuple[str, ...] = ( UCARP_ETC + "/default.sh", UCARP_ETC + "/default-up.sh", UCARP_ETC + "/default-down.sh", "ucarpboot.sh", ) - startup: tuple[str, ...] = ("bash ucarpboot.sh",) - shutdown: tuple[str, ...] = ("killall ucarp",) - validate: tuple[str, ...] = ("pidof ucarp",) + startup: Tuple[str, ...] = ("bash ucarpboot.sh",) + shutdown: Tuple[str, ...] = ("killall ucarp",) + validate: Tuple[str, ...] = ("pidof ucarp",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -44,13 +45,13 @@ class Ucarp(CoreService): Returns configuration file text. """ ucarp_bin = node.session.options.get("ucarp_bin", "/usr/sbin/ucarp") - return f"""\ + return """\ #!/bin/sh # Location of UCARP executable -UCARP_EXEC={ucarp_bin} +UCARP_EXEC=%s # Location of the UCARP config directory -UCARP_CFGDIR={UCARP_ETC} +UCARP_CFGDIR=%s # Logging Facility FACILITY=daemon @@ -91,34 +92,40 @@ OPTIONS="-z -n -M" # Send extra parameter to down and up scripts #XPARAM="-x " -XPARAM="-x ${{VIRTUAL_NET}}" +XPARAM="-x ${VIRTUAL_NET}" # The start and stop scripts -START_SCRIPT=${{UCARP_CFGDIR}}/default-up.sh -STOP_SCRIPT=${{UCARP_CFGDIR}}/default-down.sh +START_SCRIPT=${UCARP_CFGDIR}/default-up.sh +STOP_SCRIPT=${UCARP_CFGDIR}/default-down.sh # These line should not need to be touched UCARP_OPTS="$OPTIONS -b $UCARP_BASE -k $SKEW -i $INTERFACE -v $INSTANCE_ID -p $PASSWORD -u $START_SCRIPT -d $STOP_SCRIPT -a $VIRTUAL_ADDRESS -s $SOURCE_ADDRESS -f $FACILITY $XPARAM" -${{UCARP_EXEC}} -B ${{UCARP_OPTS}} -""" +${UCARP_EXEC} -B ${UCARP_OPTS} +""" % ( + ucarp_bin, + UCARP_ETC, + ) @classmethod def generate_ucarp_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the Ucarp daemons. """ - return f"""\ + return ( + """\ #!/bin/sh # Location of the UCARP config directory -UCARP_CFGDIR={UCARP_ETC} +UCARP_CFGDIR=%s -chmod a+x ${{UCARP_CFGDIR}}/*.sh +chmod a+x ${UCARP_CFGDIR}/*.sh # Start the default ucarp daemon configuration -${{UCARP_CFGDIR}}/default.sh +${UCARP_CFGDIR}/default.sh """ + % UCARP_ETC + ) @classmethod def generate_vip_up(cls, node: CoreNode) -> str: diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index e83cb9d5..54a58b2a 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -1,7 +1,7 @@ """ utility.py: defines miscellaneous utility services. """ -from typing import Optional +from typing import Optional, Tuple import netaddr @@ -27,8 +27,8 @@ class UtilService(CoreService): class IPForwardService(UtilService): name: str = "IPForward" - configs: tuple[str, ...] = ("ipforward.sh",) - startup: tuple[str, ...] = ("bash ipforward.sh",) + configs: Tuple[str, ...] = ("ipforward.sh",) + startup: Tuple[str, ...] = ("bash ipforward.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -36,30 +36,32 @@ class IPForwardService(UtilService): @classmethod def generateconfiglinux(cls, node: CoreNode, filename: str) -> str: - cfg = f"""\ + cfg = """\ #!/bin/sh # auto-generated by IPForward service (utility.py) -{SYSCTL} -w net.ipv4.conf.all.forwarding=1 -{SYSCTL} -w net.ipv4.conf.default.forwarding=1 -{SYSCTL} -w net.ipv6.conf.all.forwarding=1 -{SYSCTL} -w net.ipv6.conf.default.forwarding=1 -{SYSCTL} -w net.ipv4.conf.all.send_redirects=0 -{SYSCTL} -w net.ipv4.conf.default.send_redirects=0 -{SYSCTL} -w net.ipv4.conf.all.rp_filter=0 -{SYSCTL} -w net.ipv4.conf.default.rp_filter=0 -""" +%(sysctl)s -w net.ipv4.conf.all.forwarding=1 +%(sysctl)s -w net.ipv4.conf.default.forwarding=1 +%(sysctl)s -w net.ipv6.conf.all.forwarding=1 +%(sysctl)s -w net.ipv6.conf.default.forwarding=1 +%(sysctl)s -w net.ipv4.conf.all.send_redirects=0 +%(sysctl)s -w net.ipv4.conf.default.send_redirects=0 +%(sysctl)s -w net.ipv4.conf.all.rp_filter=0 +%(sysctl)s -w net.ipv4.conf.default.rp_filter=0 +""" % { + "sysctl": SYSCTL + } for iface in node.get_ifaces(): name = utils.sysctl_devname(iface.name) - cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.forwarding=1\n" - cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.send_redirects=0\n" - cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.rp_filter=0\n" + cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (SYSCTL, name) return cfg class DefaultRouteService(UtilService): name: str = "DefaultRoute" - configs: tuple[str, ...] = ("defaultroute.sh",) - startup: tuple[str, ...] = ("bash defaultroute.sh",) + configs: Tuple[str, ...] = ("defaultroute.sh",) + startup: Tuple[str, ...] = ("bash defaultroute.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -81,8 +83,8 @@ class DefaultRouteService(UtilService): class DefaultMulticastRouteService(UtilService): name: str = "DefaultMulticastRoute" - configs: tuple[str, ...] = ("defaultmroute.sh",) - startup: tuple[str, ...] = ("bash defaultmroute.sh",) + configs: Tuple[str, ...] = ("defaultmroute.sh",) + startup: Tuple[str, ...] = ("bash defaultmroute.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -92,7 +94,7 @@ class DefaultMulticastRouteService(UtilService): cfg += "as needed\n" for iface in node.get_ifaces(control=False): rtcmd = "ip route add 224.0.0.0/4 dev" - cfg += f"{rtcmd} {iface.name}\n" + cfg += "%s %s\n" % (rtcmd, iface.name) cfg += "\n" break return cfg @@ -100,8 +102,8 @@ class DefaultMulticastRouteService(UtilService): class StaticRouteService(UtilService): name: str = "StaticRoute" - configs: tuple[str, ...] = ("staticroute.sh",) - startup: tuple[str, ...] = ("bash staticroute.sh",) + configs: Tuple[str, ...] = ("staticroute.sh",) + startup: Tuple[str, ...] = ("bash staticroute.sh",) custom_needed: bool = True @classmethod @@ -125,16 +127,16 @@ class StaticRouteService(UtilService): if ip[-2] == ip[1]: return "" else: - rtcmd = f"#/sbin/ip route add {dst} via" - return f"{rtcmd} {ip[1]}" + rtcmd = "#/sbin/ip route add %s via" % dst + return "%s %s" % (rtcmd, ip[1]) class SshService(UtilService): name: str = "SSH" - configs: tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config") - dirs: tuple[str, ...] = ("/etc/ssh", "/var/run/sshd") - startup: tuple[str, ...] = ("bash startsshd.sh",) - shutdown: tuple[str, ...] = ("killall sshd",) + configs: Tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config") + dirs: Tuple[str, ...] = ("/etc/ssh", "/var/run/sshd") + startup: Tuple[str, ...] = ("bash startsshd.sh",) + shutdown: Tuple[str, ...] = ("killall sshd",) validation_mode: ServiceMode = ServiceMode.BLOCKING @classmethod @@ -147,22 +149,26 @@ class SshService(UtilService): sshstatedir = cls.dirs[1] sshlibdir = "/usr/lib/openssh" if filename == "startsshd.sh": - return f"""\ + return """\ #!/bin/sh # auto-generated by SSH service (utility.py) -ssh-keygen -q -t rsa -N "" -f {sshcfgdir}/ssh_host_rsa_key -chmod 655 {sshstatedir} +ssh-keygen -q -t rsa -N "" -f %s/ssh_host_rsa_key +chmod 655 %s # wait until RSA host key has been generated to launch sshd -/usr/sbin/sshd -f {sshcfgdir}/sshd_config -""" +/usr/sbin/sshd -f %s/sshd_config +""" % ( + sshcfgdir, + sshstatedir, + sshcfgdir, + ) else: - return f"""\ + return """\ # auto-generated by SSH service (utility.py) Port 22 Protocol 2 -HostKey {sshcfgdir}/ssh_host_rsa_key +HostKey %s/ssh_host_rsa_key UsePrivilegeSeparation yes -PidFile {sshstatedir}/sshd.pid +PidFile %s/sshd.pid KeyRegenerationInterval 3600 ServerKeyBits 768 @@ -191,19 +197,23 @@ PrintLastLog yes TCPKeepAlive yes AcceptEnv LANG LC_* -Subsystem sftp {sshlibdir}/sftp-server +Subsystem sftp %s/sftp-server UsePAM yes UseDNS no -""" +""" % ( + sshcfgdir, + sshstatedir, + sshlibdir, + ) class DhcpService(UtilService): name: str = "DHCP" - configs: tuple[str, ...] = ("/etc/dhcp/dhcpd.conf",) - dirs: tuple[str, ...] = ("/etc/dhcp", "/var/lib/dhcp") - startup: tuple[str, ...] = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") - shutdown: tuple[str, ...] = ("killall dhcpd",) - validate: tuple[str, ...] = ("pidof dhcpd",) + configs: Tuple[str, ...] = ("/etc/dhcp/dhcpd.conf",) + dirs: Tuple[str, ...] = ("/etc/dhcp", "/var/lib/dhcp") + startup: Tuple[str, ...] = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") + shutdown: Tuple[str, ...] = ("killall dhcpd",) + validate: Tuple[str, ...] = ("pidof dhcpd",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -242,15 +252,21 @@ ddns-update-style none; index = (ip.size - 2) / 2 rangelow = ip[index] rangehigh = ip[-2] - return f""" -subnet {ip.cidr.ip} netmask {ip.netmask} {{ - pool {{ - range {rangelow} {rangehigh}; + return """ +subnet %s netmask %s { + pool { + range %s %s; default-lease-time 600; - option routers {ip.ip}; - }} -}} -""" + option routers %s; + } +} +""" % ( + ip.cidr.ip, + ip.netmask, + rangelow, + rangehigh, + ip.ip, + ) class DhcpClientService(UtilService): @@ -259,10 +275,10 @@ class DhcpClientService(UtilService): """ name: str = "DHCPClient" - configs: tuple[str, ...] = ("startdhcpclient.sh",) - startup: tuple[str, ...] = ("bash startdhcpclient.sh",) - shutdown: tuple[str, ...] = ("killall dhclient",) - validate: tuple[str, ...] = ("pidof dhclient",) + configs: Tuple[str, ...] = ("startdhcpclient.sh",) + startup: Tuple[str, ...] = ("bash startdhcpclient.sh",) + shutdown: Tuple[str, ...] = ("killall dhclient",) + validate: Tuple[str, ...] = ("pidof dhclient",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -275,10 +291,10 @@ class DhcpClientService(UtilService): cfg += "side DNS\n# resolution based on the DHCP server response.\n" cfg += "#mkdir -p /var/run/resolvconf/interface\n" for iface in node.get_ifaces(control=False): - cfg += f"#ln -s /var/run/resolvconf/interface/{iface.name}.dhclient" + cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % iface.name cfg += " /var/run/resolvconf/resolv.conf\n" - cfg += f"/sbin/dhclient -nw -pf /var/run/dhclient-{iface.name}.pid" - cfg += f" -lf /var/run/dhclient-{iface.name}.lease {iface.name}\n" + cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % iface.name + cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (iface.name, iface.name) return cfg @@ -288,11 +304,11 @@ class FtpService(UtilService): """ name: str = "FTP" - configs: tuple[str, ...] = ("vsftpd.conf",) - dirs: tuple[str, ...] = ("/var/run/vsftpd/empty", "/var/ftp") - startup: tuple[str, ...] = ("vsftpd ./vsftpd.conf",) - shutdown: tuple[str, ...] = ("killall vsftpd",) - validate: tuple[str, ...] = ("pidof vsftpd",) + configs: Tuple[str, ...] = ("vsftpd.conf",) + dirs: Tuple[str, ...] = ("/var/run/vsftpd/empty", "/var/ftp") + startup: Tuple[str, ...] = ("vsftpd ./vsftpd.conf",) + shutdown: Tuple[str, ...] = ("killall vsftpd",) + validate: Tuple[str, ...] = ("pidof vsftpd",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -321,12 +337,12 @@ class HttpService(UtilService): """ name: str = "HTTP" - configs: tuple[str, ...] = ( + configs: Tuple[str, ...] = ( "/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html", ) - dirs: tuple[str, ...] = ( + dirs: Tuple[str, ...] = ( "/etc/apache2", "/var/run/apache2", "/var/log/apache2", @@ -334,9 +350,9 @@ class HttpService(UtilService): "/var/lock/apache2", "/var/www", ) - startup: tuple[str, ...] = ("chown www-data /var/lock/apache2", "apache2ctl start") - shutdown: tuple[str, ...] = ("apache2ctl stop",) - validate: tuple[str, ...] = ("pidof apache2",) + startup: Tuple[str, ...] = ("chown www-data /var/lock/apache2", "apache2ctl start") + shutdown: Tuple[str, ...] = ("apache2ctl stop",) + validate: Tuple[str, ...] = ("pidof apache2",) APACHEVER22: int = 22 APACHEVER24: int = 24 @@ -522,15 +538,18 @@ export LANG @classmethod def generatehtml(cls, node: CoreNode, filename: str) -> str: - body = f"""\ + body = ( + """\ -

{node.name} web server

+

%s web server

This is the default web page for this server.

The web server software is running but no content has been added, yet.

""" + % node.name + ) for iface in node.get_ifaces(control=False): - body += f"
  • {iface.name} - {[str(x) for x in iface.ips()]}
  • \n" - return f"{body}" + body += "
  • %s - %s
  • \n" % (iface.name, [str(x) for x in iface.ips()]) + return "%s" % body class PcapService(UtilService): @@ -539,10 +558,10 @@ class PcapService(UtilService): """ name: str = "pcap" - configs: tuple[str, ...] = ("pcap.sh",) - startup: tuple[str, ...] = ("bash pcap.sh start",) - shutdown: tuple[str, ...] = ("bash pcap.sh stop",) - validate: tuple[str, ...] = ("pidof tcpdump",) + configs: Tuple[str, ...] = ("pcap.sh",) + startup: Tuple[str, ...] = ("bash pcap.sh start",) + shutdown: Tuple[str, ...] = ("bash pcap.sh stop",) + validate: Tuple[str, ...] = ("pidof tcpdump",) meta: str = "logs network traffic to pcap packet capture files" @classmethod @@ -563,9 +582,11 @@ if [ "x$1" = "xstart" ]; then if iface.control: cfg += "# " redir = "< /dev/null" - cfg += ( - f"tcpdump ${{DUMPOPTS}} -w {node.name}.{iface.name}.pcap " - f"-i {iface.name} {redir} &\n" + cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % ( + node.name, + iface.name, + iface.name, + redir, ) cfg += """ @@ -579,13 +600,13 @@ fi; class RadvdService(UtilService): name: str = "radvd" - configs: tuple[str, ...] = ("/etc/radvd/radvd.conf",) - dirs: tuple[str, ...] = ("/etc/radvd", "/var/run/radvd") - startup: tuple[str, ...] = ( + configs: Tuple[str, ...] = ("/etc/radvd/radvd.conf",) + dirs: Tuple[str, ...] = ("/etc/radvd", "/var/run/radvd") + startup: Tuple[str, ...] = ( "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log", ) - shutdown: tuple[str, ...] = ("pkill radvd",) - validate: tuple[str, ...] = ("pidof radvd",) + shutdown: Tuple[str, ...] = ("pkill radvd",) + validate: Tuple[str, ...] = ("pidof radvd",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -598,26 +619,32 @@ class RadvdService(UtilService): prefixes = list(map(cls.subnetentry, iface.ips())) if len(prefixes) < 1: continue - cfg += f"""\ -interface {iface.name} -{{ + cfg += ( + """\ +interface %s +{ AdvSendAdvert on; MinRtrAdvInterval 3; MaxRtrAdvInterval 10; AdvDefaultPreference low; AdvHomeAgentFlag off; """ + % iface.name + ) for prefix in prefixes: if prefix == "": continue - cfg += f"""\ - prefix {prefix} - {{ + cfg += ( + """\ + prefix %s + { AdvOnLink on; AdvAutonomous on; AdvRouterAddr on; - }}; + }; """ + % prefix + ) cfg += "};\n" return cfg @@ -640,10 +667,10 @@ class AtdService(UtilService): """ name: str = "atd" - configs: tuple[str, ...] = ("startatd.sh",) - dirs: tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") - startup: tuple[str, ...] = ("bash startatd.sh",) - shutdown: tuple[str, ...] = ("pkill atd",) + configs: Tuple[str, ...] = ("startatd.sh",) + dirs: Tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") + startup: Tuple[str, ...] = ("bash startatd.sh",) + shutdown: Tuple[str, ...] = ("pkill atd",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index ac29b299..485fe159 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -2,7 +2,7 @@ xorp.py: defines routing services provided by the XORP routing suite. """ -from typing import Optional +from typing import Optional, Tuple import netaddr @@ -19,14 +19,15 @@ class XorpRtrmgr(CoreService): name: str = "xorp_rtrmgr" group: str = "XORP" - executables: tuple[str, ...] = ("xorp_rtrmgr",) - dirs: tuple[str, ...] = ("/etc/xorp",) - configs: tuple[str, ...] = ("/etc/xorp/config.boot",) - startup: tuple[ - str, ... - ] = f"xorp_rtrmgr -d -b {configs[0]} -l /var/log/{name}.log -P /var/run/{name}.pid" - shutdown: tuple[str, ...] = ("killall xorp_rtrmgr",) - validate: tuple[str, ...] = ("pidof xorp_rtrmgr",) + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dirs: Tuple[str, ...] = ("/etc/xorp",) + configs: Tuple[str, ...] = ("/etc/xorp/config.boot",) + startup: Tuple[str, ...] = ( + "xorp_rtrmgr -d -b %s -l /var/log/%s.log -P /var/run/%s.pid" + % (configs[0], name, name), + ) + shutdown: Tuple[str, ...] = ("killall xorp_rtrmgr",) + validate: Tuple[str, ...] = ("pidof xorp_rtrmgr",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -37,8 +38,8 @@ class XorpRtrmgr(CoreService): """ cfg = "interfaces {\n" for iface in node.get_ifaces(): - cfg += f" interface {iface.name} {{\n" - cfg += f"\tvif {iface.name} {{\n" + cfg += " interface %s {\n" % iface.name + cfg += "\tvif %s {\n" % iface.name cfg += "".join(map(cls.addrstr, iface.ips())) cfg += cls.lladdrstr(iface) cfg += "\t}\n" @@ -58,8 +59,8 @@ class XorpRtrmgr(CoreService): """ helper for mapping IP addresses to XORP config statements """ - cfg = f"\t address {ip.ip} {{\n" - cfg += f"\t\tprefix-length: {ip.prefixlen}\n" + cfg = "\t address %s {\n" % ip.ip + cfg += "\t\tprefix-length: %s\n" % ip.prefixlen cfg += "\t }\n" return cfg @@ -68,7 +69,7 @@ class XorpRtrmgr(CoreService): """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = f"\t address {iface.mac.eui64()} {{\n" + cfg = "\t address %s {\n" % iface.mac.eui64() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -82,8 +83,8 @@ class XorpService(CoreService): name: Optional[str] = None group: str = "XORP" - executables: tuple[str, ...] = ("xorp_rtrmgr",) - dependencies: tuple[str, ...] = ("xorp_rtrmgr",) + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dependencies: Tuple[str, ...] = ("xorp_rtrmgr",) meta: str = ( "The config file for this service can be found in the xorp_rtrmgr service." ) @@ -94,7 +95,7 @@ class XorpService(CoreService): Helper to add a forwarding engine entry to the config file. """ cfg = "fea {\n" - cfg += f" {forwarding} {{\n" + cfg += " %s {\n" % forwarding cfg += "\tdisable:false\n" cfg += " }\n" cfg += "}\n" @@ -110,10 +111,10 @@ class XorpService(CoreService): names.append(iface.name) names.append("register_vif") cfg = "plumbing {\n" - cfg += f" {forwarding} {{\n" + cfg += " %s {\n" % forwarding for name in names: - cfg += f"\tinterface {name} {{\n" - cfg += f"\t vif {name} {{\n" + cfg += "\tinterface %s {\n" % name + cfg += "\t vif %s {\n" % name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" @@ -172,13 +173,13 @@ class XorpOspfv2(XorpService): rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf4 {\n" - cfg += f"\trouter-id: {rtrid}\n" + cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" for iface in node.get_ifaces(control=False): - cfg += f"\t interface {iface.name} {{\n" - cfg += f"\t\tvif {iface.name} {{\n" + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name for ip4 in iface.ip4s: - cfg += f"\t\t address {ip4.ip} {{\n" + cfg += "\t\t address %s {\n" % ip4.ip cfg += "\t\t }\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -203,11 +204,11 @@ class XorpOspfv3(XorpService): rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf6 0 { /* Instance ID 0 */\n" - cfg += f"\trouter-id: {rtrid}\n" + cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" for iface in node.get_ifaces(control=False): - cfg += f"\t interface {iface.name} {{\n" - cfg += f"\t\tvif {iface.name} {{\n" + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" @@ -233,7 +234,7 @@ class XorpBgp(XorpService): rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " bgp {\n" - cfg += f"\tbgp-id: {rtrid}\n" + cfg += "\tbgp-id: %s\n" % rtrid cfg += "\tlocal-as: 65001 /* change this */\n" cfg += '\texport: "export-connected"\n' cfg += "\tpeer 10.0.1.1 { /* change this */\n" @@ -261,10 +262,10 @@ class XorpRip(XorpService): cfg += " rip {\n" cfg += '\texport: "export-connected"\n' for iface in node.get_ifaces(control=False): - cfg += f"\tinterface {iface.name} {{\n" - cfg += f"\t vif {iface.name} {{\n" + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name for ip4 in iface.ip4s: - cfg += f"\t\taddress {ip4.ip} {{\n" + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -289,9 +290,9 @@ class XorpRipng(XorpService): cfg += " ripng {\n" cfg += '\texport: "export-connected"\n' for iface in node.get_ifaces(control=False): - cfg += f"\tinterface {iface.name} {{\n" - cfg += f"\t vif {iface.name} {{\n" - cfg += f"\t\taddress {iface.mac.eui64()} {{\n" + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + cfg += "\t\taddress %s {\n" % iface.mac.eui64() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -316,8 +317,8 @@ class XorpPimSm4(XorpService): names = [] for iface in node.get_ifaces(control=False): names.append(iface.name) - cfg += f"\tinterface {iface.name} {{\n" - cfg += f"\t vif {iface.name} {{\n" + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" @@ -328,20 +329,20 @@ class XorpPimSm4(XorpService): names.append("register_vif") for name in names: - cfg += f"\tinterface {name} {{\n" - cfg += f"\t vif {name} {{\n" + cfg += "\tinterface %s {\n" % name + cfg += "\t vif %s {\n" % name cfg += "\t\tdr-priority: 1\n" cfg += "\t }\n" cfg += "\t}\n" cfg += "\tbootstrap {\n" cfg += "\t cand-bsr {\n" cfg += "\t\tscope-zone 224.0.0.0/4 {\n" - cfg += f'\t\t cand-bsr-by-vif-name: "{names[0]}"\n' + cfg += '\t\t cand-bsr-by-vif-name: "%s"\n' % names[0] cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t cand-rp {\n" cfg += "\t\tgroup-prefix 224.0.0.0/4 {\n" - cfg += f'\t\t cand-rp-by-vif-name: "{names[0]}"\n' + cfg += '\t\t cand-rp-by-vif-name: "%s"\n' % names[0] cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" @@ -370,8 +371,8 @@ class XorpPimSm6(XorpService): names = [] for iface in node.get_ifaces(control=False): names.append(iface.name) - cfg += f"\tinterface {iface.name} {{\n" - cfg += f"\t vif {iface.name} {{\n" + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" @@ -382,20 +383,20 @@ class XorpPimSm6(XorpService): names.append("register_vif") for name in names: - cfg += f"\tinterface {name} {{\n" - cfg += f"\t vif {name} {{\n" + cfg += "\tinterface %s {\n" % name + cfg += "\t vif %s {\n" % name cfg += "\t\tdr-priority: 1\n" cfg += "\t }\n" cfg += "\t}\n" cfg += "\tbootstrap {\n" cfg += "\t cand-bsr {\n" cfg += "\t\tscope-zone ff00::/8 {\n" - cfg += f'\t\t cand-bsr-by-vif-name: "{names[0]}"\n' + cfg += '\t\t cand-bsr-by-vif-name: "%s"\n' % names[0] cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t cand-rp {\n" cfg += "\t\tgroup-prefix ff00::/8 {\n" - cfg += f'\t\t cand-rp-by-vif-name: "{names[0]}"\n' + cfg += '\t\t cand-rp-by-vif-name: "%s"\n' % names[0] cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" @@ -422,12 +423,12 @@ class XorpOlsr(XorpService): rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " olsr4 {\n" - cfg += f"\tmain-address: {rtrid}\n" + cfg += "\tmain-address: %s\n" % rtrid for iface in node.get_ifaces(control=False): - cfg += f"\tinterface {iface.name} {{\n" - cfg += f"\t vif {iface.name} {{\n" + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name for ip4 in iface.ip4s: - cfg += f"\t\taddress {ip4.ip} {{\n" + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" diff --git a/daemon/core/utils.py b/daemon/core/utils.py index df00984c..244590f8 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -17,11 +17,23 @@ import shutil import sys import threading from collections import OrderedDict -from collections.abc import Iterable from pathlib import Path from queue import Queue from subprocess import PIPE, STDOUT, Popen -from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + Iterable, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) import netaddr @@ -58,7 +70,7 @@ def execute_script(coreemu: "CoreEmu", file_path: Path, args: str) -> None: def execute_file( - path: Path, exec_globals: dict[str, str] = None, exec_locals: dict[str, str] = None + path: Path, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None ) -> None: """ Provides a way to execute a file. @@ -87,7 +99,7 @@ def hashkey(value: Union[str, int]) -> int: """ if isinstance(value, int): value = str(value) - value = value.encode() + value = value.encode("utf-8") return int(hashlib.sha256(value).hexdigest(), 16) @@ -119,7 +131,7 @@ def _valid_module(path: Path) -> bool: return True -def _is_class(module: Any, member: type, clazz: type) -> bool: +def _is_class(module: Any, member: Type, clazz: Type) -> bool: """ Validates if a module member is a class and an instance of a CoreService. @@ -163,7 +175,7 @@ def which(command: str, required: bool) -> str: return found_path -def make_tuple_fromstr(s: str, value_type: Callable[[str], T]) -> tuple[T]: +def make_tuple_fromstr(s: str, value_type: Callable[[str], T]) -> Tuple[T]: """ Create a tuple from a string. @@ -181,7 +193,7 @@ def make_tuple_fromstr(s: str, value_type: Callable[[str], T]) -> tuple[T]: return tuple(value_type(i) for i in values) -def mute_detach(args: str, **kwargs: dict[str, Any]) -> int: +def mute_detach(args: str, **kwargs: Dict[str, Any]) -> int: """ Run a muted detached process by forking it. @@ -198,7 +210,7 @@ def mute_detach(args: str, **kwargs: dict[str, Any]) -> int: def cmd( args: str, - env: dict[str, str] = None, + env: Dict[str, str] = None, cwd: Path = None, wait: bool = True, shell: bool = False, @@ -224,9 +236,9 @@ def cmd( p = Popen(args, stdout=output, stderr=output, env=env, cwd=cwd, shell=shell) if wait: stdout, stderr = p.communicate() - stdout = stdout.decode().strip() - stderr = stderr.decode().strip() - status = p.returncode + stdout = stdout.decode("utf-8").strip() + stderr = stderr.decode("utf-8").strip() + status = p.wait() if status != 0: raise CoreCommandError(status, input_args, stdout, stderr) return stdout @@ -237,7 +249,7 @@ def cmd( raise CoreCommandError(1, input_args, "", e.strerror) -def run_cmds(args: list[str], wait: bool = True, shell: bool = False) -> list[str]: +def run_cmds(args: List[str], wait: bool = True, shell: bool = False) -> List[str]: """ Execute a series of commands on the host and returns a list of the combined stderr stdout output. @@ -282,7 +294,7 @@ def file_demunge(pathname: str, header: str) -> None: :param header: header text to target for removal :return: nothing """ - with open(pathname) as read_file: + with open(pathname, "r") as read_file: lines = read_file.readlines() start = None @@ -336,7 +348,7 @@ def sysctl_devname(devname: str) -> Optional[str]: return devname.replace(".", "/") -def load_config(file_path: Path, d: dict[str, str]) -> None: +def load_config(file_path: Path, d: Dict[str, str]) -> None: """ Read key=value pairs from a file, into a dict. Skip comments; strip newline characters and spacing. @@ -357,7 +369,7 @@ def load_config(file_path: Path, d: dict[str, str]) -> None: logger.exception("error reading file to dict: %s", file_path) -def load_module(import_statement: str, clazz: Generic[T]) -> list[T]: +def load_module(import_statement: str, clazz: Generic[T]) -> List[T]: classes = [] try: module = importlib.import_module(import_statement) @@ -372,7 +384,7 @@ def load_module(import_statement: str, clazz: Generic[T]) -> list[T]: return classes -def load_classes(path: Path, clazz: Generic[T]) -> list[T]: +def load_classes(path: Path, clazz: Generic[T]) -> List[T]: """ Dynamically load classes for use within CORE. @@ -414,16 +426,18 @@ def load_logging_config(config_path: Path) -> None: def run_cmds_threaded( - node_cmds: list[tuple["CoreNode", list[str]]], + nodes: List["CoreNode"], + cmds: List[str], wait: bool = True, shell: bool = False, workers: int = None, -) -> tuple[dict[int, list[str]], list[Exception]]: +) -> Tuple[Dict[int, List[str]], List[Exception]]: """ - Run the set of commands for the node provided. Each node will + Run a set of commands in order across a provided set of nodes. Each node will run the commands within the context of a threadpool. - :param node_cmds: list of tuples of nodes and commands to run within them + :param nodes: nodes to run commands in + :param cmds: commands to run in nodes :param wait: True to wait for status, False otherwise :param shell: True to run shell like, False otherwise :param workers: number of workers for threadpool, uses library default otherwise @@ -432,18 +446,18 @@ def run_cmds_threaded( """ def _node_cmds( - _target: "CoreNode", _cmds: list[str], _wait: bool, _shell: bool - ) -> list[str]: - cmd_outputs = [] + _target: "CoreNode", _cmds: List[str], _wait: bool, _shell: bool + ) -> List[str]: + outputs = [] for _cmd in _cmds: output = _target.cmd(_cmd, wait=_wait, shell=_shell) - cmd_outputs.append(output) - return cmd_outputs + outputs.append(output) + return outputs with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: futures = [] node_mappings = {} - for node, cmds in node_cmds: + for node in nodes: future = executor.submit(_node_cmds, node, cmds, wait, shell) node_mappings[future] = node futures.append(future) @@ -461,17 +475,19 @@ def run_cmds_threaded( def run_cmds_mp( - node_cmds: list[tuple["CoreNode", list[str]]], + nodes: List["CoreNode"], + cmds: List[str], wait: bool = True, shell: bool = False, workers: int = None, -) -> tuple[dict[int, list[str]], list[Exception]]: +) -> Tuple[Dict[int, List[str]], List[Exception]]: """ - Run the set of commands for the node provided. Each node will + Run a set of commands in order across a provided set of nodes. Each node will run the commands within the context of a process pool. This will not work for distributed nodes and throws an exception when encountered. - :param node_cmds: list of tuples of nodes and commands to run within them + :param nodes: nodes to run commands in + :param cmds: commands to run in nodes :param wait: True to wait for status, False otherwise :param shell: True to run shell like, False otherwise :param workers: number of workers for threadpool, uses library default otherwise @@ -482,7 +498,7 @@ def run_cmds_mp( with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor: futures = [] node_mapping = {} - for node, cmds in node_cmds: + for node in nodes: node_cmds = [node.create_cmd(x) for x in cmds] if node.server: raise CoreError( @@ -505,8 +521,8 @@ def run_cmds_mp( def threadpool( - funcs: list[tuple[Callable, Iterable[Any], dict[Any, Any]]], workers: int = 10 -) -> tuple[list[Any], list[Exception]]: + funcs: List[Tuple[Callable, Iterable[Any], Dict[Any, Any]]], workers: int = 10 +) -> Tuple[List[Any], List[Exception]]: """ Run provided functions, arguments, and keywords within a threadpool collecting results and exceptions. @@ -559,7 +575,7 @@ def iface_config_id(node_id: int, iface_id: int = None) -> int: return node_id -def parse_iface_config_id(config_id: int) -> tuple[int, Optional[int]]: +def parse_iface_config_id(config_id: int) -> Tuple[int, Optional[int]]: """ Parses configuration id, that may be potentially derived from an interface for a node. diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index d566b501..a483a8ee 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar from lxml import etree @@ -15,9 +15,8 @@ from core.errors import CoreXmlError from core.nodes.base import CoreNodeBase, CoreNodeOptions, NodeBase, Position from core.nodes.docker import DockerNode, DockerOptions from core.nodes.interface import CoreInterface -from core.nodes.lxd import LxcNode, LxcOptions +from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode -from core.nodes.podman import PodmanNode, PodmanOptions from core.nodes.wireless import WirelessNode from core.services.coreservices import CoreService @@ -27,7 +26,7 @@ if TYPE_CHECKING: from core.emane.emanemodel import EmaneModel from core.emulator.session import Session - EmaneModelType = type[EmaneModel] + EmaneModelType = Type[EmaneModel] T = TypeVar("T") @@ -87,7 +86,7 @@ def create_iface_data(iface_element: etree.Element) -> InterfaceData: def create_emane_model_config( node_id: int, model: "EmaneModelType", - config: dict[str, str], + config: Dict[str, str], iface_id: Optional[int], ) -> etree.Element: emane_element = etree.Element("emane_configuration") @@ -149,8 +148,8 @@ class NodeElement: class ServiceElement: - def __init__(self, service: type[CoreService]) -> None: - self.service: type[CoreService] = service + def __init__(self, service: Type[CoreService]) -> None: + self.service: Type[CoreService] = service self.element: etree.Element = etree.Element("service") add_attribute(self.element, "name", service.name) self.add_directories() @@ -226,9 +225,6 @@ class DeviceElement(NodeElement): elif isinstance(self.node, LxcNode): clazz = "lxc" image = self.node.image - elif isinstance(self.node, PodmanNode): - clazz = "podman" - image = self.node.image add_attribute(self.element, "class", clazz) add_attribute(self.element, "image", image) @@ -270,7 +266,7 @@ class NetworkElement(NodeElement): node_type = self.session.get_node_type(type(self.node)) add_attribute(self.element, "type", node_type.name) - def add_wireless_config(self, config: dict[str, Configuration]) -> None: + def add_wireless_config(self, config: Dict[str, Configuration]) -> None: wireless_element = etree.SubElement(self.element, "wireless") for config_item in config.values(): add_configuration(wireless_element, config_item.id, config_item.default) @@ -812,8 +808,6 @@ class CoreXmlReader: node_type = NodeTypes.DOCKER elif clazz == "lxc": node_type = NodeTypes.LXC - elif clazz == "podman": - node_type = NodeTypes.PODMAN _class = self.session.get_node_class(node_type) options = _class.create_options() options.icon = icon @@ -831,7 +825,7 @@ class CoreXmlReader: options.config_services.extend( x.get("name") for x in config_service_elements.iterchildren() ) - if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)): + if isinstance(options, DockerOptions): options.image = image # get position information position_element = device_element.find("position") diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 0b38e9b0..c062a1d2 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -1,6 +1,6 @@ import os import socket -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Tuple import netaddr from lxml import etree @@ -78,7 +78,7 @@ def get_address_type(address: str) -> str: return address_type -def get_ipv4_addresses(hostname: str) -> list[tuple[str, str]]: +def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: if hostname == "localhost": addresses = [] args = f"{IP} -o -f inet address show" diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 4b8ada70..91d8ce28 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -1,7 +1,7 @@ import logging from pathlib import Path from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from lxml import etree @@ -22,7 +22,7 @@ if TYPE_CHECKING: _MAC_PREFIX = "02:02" -def is_external(config: dict[str, str]) -> bool: +def is_external(config: Dict[str, str]) -> bool: """ Checks if the configuration is for an external transport. @@ -32,7 +32,7 @@ def is_external(config: dict[str, str]) -> bool: return config.get("external") == "1" -def _value_to_params(value: str) -> Optional[tuple[str]]: +def _value_to_params(value: str) -> Optional[Tuple[str]]: """ Helper to convert a parameter to a parameter tuple. @@ -113,9 +113,9 @@ def add_param(xml_element: etree.Element, name: str, value: str) -> None: def add_configurations( xml_element: etree.Element, - configurations: list[Configuration], - config: dict[str, str], - config_ignore: set[str], + configurations: List[Configuration], + config: Dict[str, str], + config_ignore: Set, ) -> None: """ Add emane model configurations to xml element. @@ -148,7 +148,7 @@ def build_platform_xml( nem_port: int, emane_net: EmaneNet, iface: CoreInterface, - config: dict[str, str], + config: Dict[str, str], ) -> None: """ Create platform xml for a nem/interface. @@ -209,7 +209,7 @@ def build_platform_xml( create_node_file(iface.node, platform_element, doc_name, file_name) -def create_transport_xml(iface: CoreInterface, config: dict[str, str]) -> None: +def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: """ Build transport xml file for node and transport type. @@ -240,7 +240,7 @@ def create_transport_xml(iface: CoreInterface, config: dict[str, str]) -> None: def create_phy_xml( - emane_model: "EmaneModel", iface: CoreInterface, config: dict[str, str] + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the phy xml document. @@ -261,7 +261,7 @@ def create_phy_xml( def create_mac_xml( - emane_model: "EmaneModel", iface: CoreInterface, config: dict[str, str] + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the mac xml document. @@ -284,7 +284,7 @@ def create_mac_xml( def create_nem_xml( - emane_model: "EmaneModel", iface: CoreInterface, config: dict[str, str] + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the nem xml document. diff --git a/daemon/poetry.lock b/daemon/poetry.lock index c2aae40d..6278e3e9 100644 --- a/daemon/poetry.lock +++ b/daemon/poetry.lock @@ -1,6 +1,14 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "atomicwrites" -version = "1.4.1" +version = "1.4.0" description = "Atomic file writes." category = "dev" optional = false @@ -8,65 +16,61 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "22.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests_no_zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "bcrypt" -version = "4.0.1" +version = "3.2.2" description = "Modern password hashing for your software and your servers" category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +cffi = ">=1.1" + [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] [[package]] name = "black" -version = "22.12.0" +version = "19.3b0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +toml = ">=0.9.4" [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false -python-versions = ">=3.6" +python-versions = "*" [[package]] name = "cffi" -version = "1.15.1" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -77,34 +81,35 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.3.1" +version = "3.0.0" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.6" [[package]] name = "click" -version = "8.1.3" +version = "8.0.4" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.6" +version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cryptography" -version = "39.0.1" +version = "37.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -114,18 +119,24 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] -sdist = ["setuptools-rust (>=0.11.4)"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] -test-randomorder = ["pytest-randomly"] -tox = ["tox"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = ">=3.6, <3.7" [[package]] name = "distlib" -version = "0.3.6" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -133,7 +144,7 @@ python-versions = "*" [[package]] name = "fabric" -version = "2.7.1" +version = "2.5.0" description = "High level SSH command execution" category = "main" optional = false @@ -142,7 +153,6 @@ python-versions = "*" [package.dependencies] invoke = ">=1.3,<2.0" paramiko = ">=2.4" -pathlib2 = "*" [package.extras] pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] @@ -150,15 +160,15 @@ testing = ["mock (>=2.0.0,<3.0)"] [[package]] name = "filelock" -version = "3.9.0" +version = "3.4.1" description = "A platform independent file lock." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] name = "flake8" @@ -169,56 +179,83 @@ optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" [[package]] name = "grpcio" -version = "1.54.2" +version = "1.43.0" description = "HTTP/2-based RPC framework" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" + +[package.dependencies] +six = ">=1.5.2" [package.extras] -protobuf = ["grpcio-tools (>=1.54.2)"] +protobuf = ["grpcio-tools (>=1.43.0)"] [[package]] name = "grpcio-tools" -version = "1.54.2" +version = "1.43.0" description = "Protobuf code generator for gRPC" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] -grpcio = ">=1.54.2" -protobuf = ">=4.21.6,<5.0dev" -setuptools = "*" +grpcio = ">=1.43.0" +protobuf = ">=3.5.0.post1,<4.0dev" [[package]] name = "identify" -version = "2.5.18" +version = "1.6.2" description = "File identification library for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.extras] -license = ["ukkonen"] +license = ["editdistance"] [[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" +name = "importlib-metadata" +version = "4.8.3" +description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "importlib-resources" +version = "5.4.0" +description = "Read resources from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] [[package]] name = "invoke" -version = "1.7.3" +version = "1.4.1" description = "Pythonic task execution" category = "main" optional = false @@ -235,12 +272,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] pipfile = ["pipreqs", "requirementslib"] pyproject = ["toml"] -requirements = ["pip-api", "pipreqs"] +requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs (>=1.4.0)"] [[package]] name = "lxml" -version = "4.9.1" +version = "4.6.5" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -249,32 +286,31 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] +htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] [[package]] -name = "Mako" -version = "1.2.3" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +name = "mako" +version = "1.1.3" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] MarkupSafe = ">=0.9.2" [package.extras] -babel = ["Babel"] +babel = ["babel"] lingua = ["lingua"] -testing = ["pytest"] [[package]] -name = "MarkupSafe" -version = "2.1.2" +name = "markupsafe" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "mccabe" @@ -293,14 +329,14 @@ optional = false python-versions = ">=3.6" [package.extras] -build = ["blurb", "twine", "wheel"] +build = ["twine", "wheel", "blurb"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] [[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." +name = "more-itertools" +version = "8.13.0" +description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = ">=3.5" @@ -315,95 +351,76 @@ python-versions = "*" [[package]] name = "nodeenv" -version = "1.7.0" +version = "1.6.0" description = "Node.js virtual environment builder" category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" - -[package.dependencies] -setuptools = "*" +python-versions = "*" [[package]] name = "packaging" -version = "23.0" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.7" - -[[package]] -name = "paramiko" -version = "3.0.0" -description = "SSH2 protocol library" -category = "main" -optional = false python-versions = ">=3.6" [package.dependencies] -bcrypt = ">=3.2" -cryptography = ">=3.3" -pynacl = ">=1.5" - -[package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=2.0)"] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] -name = "pathlib2" -version = "2.3.7.post1" -description = "Object-oriented filesystem paths" +name = "paramiko" +version = "2.10.4" +description = "SSH2 protocol library" category = "main" optional = false python-versions = "*" [package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" six = "*" -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] [[package]] -name = "Pillow" -version = "9.4.0" +name = "pillow" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "3.0.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.6" +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -416,6 +433,8 @@ python-versions = ">=3.6" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" @@ -423,11 +442,11 @@ virtualenv = ">=15.2" [[package]] name = "protobuf" -version = "4.21.9" -description = "" +version = "3.19.4" +description = "Protocol Buffers" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.5" [[package]] name = "py" @@ -462,7 +481,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "PyNaCl" +name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "main" @@ -473,61 +492,61 @@ python-versions = ">=3.6" cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyproj" -version = "3.3.1" +version = "3.0.1" description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" [package.dependencies] certifi = "*" [[package]] name = "pytest" -version = "6.2.5" +version = "5.4.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.5" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" +attrs = ">=17.4.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +more-itertools = ">=4.0.0" packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" [package.extras] +checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] -name = "PyYAML" -version = "6.0.1" +name = "pyyaml" +version = "5.4" description = "YAML parser and emitter for Python" category = "main" optional = false -python-versions = ">=3.6" - -[[package]] -name = "setuptools" -version = "67.4.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "six" @@ -545,449 +564,450 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "virtualenv" -version = "20.19.0" +version = "20.14.1" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "10902a50368c4381aec5a3e72a221a4c4225ae1be17ee38600f89aaee4a49c1f" +python-versions = "^3.6" +content-hash = "0a739e4b479c0c2111fa22ffb1ed55a99d26583a576a1b548204c29b726e3e33" [metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] bcrypt = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, + {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, + {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, + {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, + {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, + {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, ] certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, + {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, ] click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] cryptography = [ - {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"}, - {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"}, - {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"}, - {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"}, - {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, - {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"}, - {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, + {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, + {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, + {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, + {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, + {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, + {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, + {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, + {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, + {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] fabric = [ - {file = "fabric-2.7.1-py2.py3-none-any.whl", hash = "sha256:7610362318ef2d391cc65d4befb684393975d889ed5720f23499394ec0e136fa"}, - {file = "fabric-2.7.1.tar.gz", hash = "sha256:76f8fef59cf2061dbd849bbce4fe49bdd820884385004b0ca59136ac3db129e4"}, + {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, + {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, ] filelock = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, + {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"}, + {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"}, ] flake8 = [ {file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"}, {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"}, ] grpcio = [ - {file = "grpcio-1.54.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:40e1cbf69d6741b40f750f3cccc64326f927ac6145a9914d33879e586002350c"}, - {file = "grpcio-1.54.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2288d76e4d4aa7ef3fe7a73c1c470b66ea68e7969930e746a8cd8eca6ef2a2ea"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c0e3155fc5335ec7b3b70f15230234e529ca3607b20a562b6c75fb1b1218874c"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bf88004fe086c786dc56ef8dd6cb49c026833fdd6f42cb853008bce3f907148"}, - {file = "grpcio-1.54.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be88c081e33f20630ac3343d8ad9f1125f32987968e9c8c75c051c9800896e8"}, - {file = "grpcio-1.54.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:33d40954199bddbb6a78f8f6f2b2082660f381cd2583ec860a6c2fa7c8400c08"}, - {file = "grpcio-1.54.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b52d00d1793d290c81ad6a27058f5224a7d5f527867e5b580742e1bd211afeee"}, - {file = "grpcio-1.54.2-cp310-cp310-win32.whl", hash = "sha256:881d058c5ccbea7cc2c92085a11947b572498a27ef37d3eef4887f499054dca8"}, - {file = "grpcio-1.54.2-cp310-cp310-win_amd64.whl", hash = "sha256:0212e2f7fdf7592e4b9d365087da30cb4d71e16a6f213120c89b4f8fb35a3ab3"}, - {file = "grpcio-1.54.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:1e623e0cf99a0ac114f091b3083a1848dbc64b0b99e181473b5a4a68d4f6f821"}, - {file = "grpcio-1.54.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:66233ccd2a9371158d96e05d082043d47dadb18cbb294dc5accfdafc2e6b02a7"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:4cb283f630624ebb16c834e5ac3d7880831b07cbe76cb08ab7a271eeaeb8943e"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a1e601ee31ef30a9e2c601d0867e236ac54c922d32ed9f727b70dd5d82600d5"}, - {file = "grpcio-1.54.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8da84bbc61a4e92af54dc96344f328e5822d574f767e9b08e1602bb5ddc254a"}, - {file = "grpcio-1.54.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5008964885e8d23313c8e5ea0d44433be9bfd7e24482574e8cc43c02c02fc796"}, - {file = "grpcio-1.54.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a2f5a1f1080ccdc7cbaf1171b2cf384d852496fe81ddedeb882d42b85727f610"}, - {file = "grpcio-1.54.2-cp311-cp311-win32.whl", hash = "sha256:b74ae837368cfffeb3f6b498688a123e6b960951be4dec0e869de77e7fa0439e"}, - {file = "grpcio-1.54.2-cp311-cp311-win_amd64.whl", hash = "sha256:8cdbcbd687e576d48f7886157c95052825ca9948c0ed2afdc0134305067be88b"}, - {file = "grpcio-1.54.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:782f4f8662a2157c4190d0f99eaaebc602899e84fb1e562a944e5025929e351c"}, - {file = "grpcio-1.54.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:714242ad0afa63a2e6dabd522ae22e1d76e07060b5af2ddda5474ba4f14c2c94"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:f900ed4ad7a0f1f05d35f955e0943944d5a75f607a836958c6b8ab2a81730ef2"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96a41817d2c763b1d0b32675abeb9179aa2371c72aefdf74b2d2b99a1b92417b"}, - {file = "grpcio-1.54.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70fcac7b94f4c904152809a050164650ac81c08e62c27aa9f156ac518029ebbe"}, - {file = "grpcio-1.54.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fd6c6c29717724acf9fc1847c4515d57e4dc12762452457b9cb37461f30a81bb"}, - {file = "grpcio-1.54.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c2392f5b5d84b71d853918687d806c1aa4308109e5ca158a16e16a6be71041eb"}, - {file = "grpcio-1.54.2-cp37-cp37m-win_amd64.whl", hash = "sha256:51630c92591d6d3fe488a7c706bd30a61594d144bac7dee20c8e1ce78294f474"}, - {file = "grpcio-1.54.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:b04202453941a63b36876a7172b45366dc0cde10d5fd7855c0f4a4e673c0357a"}, - {file = "grpcio-1.54.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:89dde0ac72a858a44a2feb8e43dc68c0c66f7857a23f806e81e1b7cc7044c9cf"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:09d4bfd84686cd36fd11fd45a0732c7628308d094b14d28ea74a81db0bce2ed3"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fc2b4edb938c8faa4b3c3ea90ca0dd89b7565a049e8e4e11b77e60e4ed2cc05"}, - {file = "grpcio-1.54.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61f7203e2767800edee7a1e1040aaaf124a35ce0c7fe0883965c6b762defe598"}, - {file = "grpcio-1.54.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e416c8baf925b5a1aff31f7f5aecc0060b25d50cce3a5a7255dc5cf2f1d4e5eb"}, - {file = "grpcio-1.54.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dc80c9c6b608bf98066a038e0172013a49cfa9a08d53335aefefda2c64fc68f4"}, - {file = "grpcio-1.54.2-cp38-cp38-win32.whl", hash = "sha256:8d6192c37a30a115f4663592861f50e130caed33efc4eec24d92ec881c92d771"}, - {file = "grpcio-1.54.2-cp38-cp38-win_amd64.whl", hash = "sha256:46a057329938b08e5f0e12ea3d7aed3ecb20a0c34c4a324ef34e00cecdb88a12"}, - {file = "grpcio-1.54.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:2296356b5c9605b73ed6a52660b538787094dae13786ba53080595d52df13a98"}, - {file = "grpcio-1.54.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:c72956972e4b508dd39fdc7646637a791a9665b478e768ffa5f4fe42123d5de1"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:9bdbb7624d65dc0ed2ed8e954e79ab1724526f09b1efa88dcd9a1815bf28be5f"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c44e1a765b31e175c391f22e8fc73b2a2ece0e5e6ff042743d8109b5d2eff9f"}, - {file = "grpcio-1.54.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cc928cfe6c360c1df636cf7991ab96f059666ac7b40b75a769410cc6217df9c"}, - {file = "grpcio-1.54.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a08920fa1a97d4b8ee5db2f31195de4a9def1a91bc003544eb3c9e6b8977960a"}, - {file = "grpcio-1.54.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4864f99aac207e3e45c5e26c6cbb0ad82917869abc2f156283be86c05286485c"}, - {file = "grpcio-1.54.2-cp39-cp39-win32.whl", hash = "sha256:b38b3de8cff5bc70f8f9c615f51b48eff7313fc9aca354f09f81b73036e7ddfa"}, - {file = "grpcio-1.54.2-cp39-cp39-win_amd64.whl", hash = "sha256:be48496b0e00460717225e7680de57c38be1d8629dc09dadcd1b3389d70d942b"}, - {file = "grpcio-1.54.2.tar.gz", hash = "sha256:50a9f075eeda5097aa9a182bb3877fe1272875e45370368ac0ee16ab9e22d019"}, + {file = "grpcio-1.43.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:a4e786a8ee8b30b25d70ee52cda6d1dbba2a8ca2f1208d8e20ed8280774f15c8"}, + {file = "grpcio-1.43.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:af9c3742f6c13575c0d4147a8454da0ff5308c4d9469462ff18402c6416942fe"}, + {file = "grpcio-1.43.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:fdac966699707b5554b815acc272d81e619dd0999f187cd52a61aef075f870ee"}, + {file = "grpcio-1.43.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e463b4aa0a6b31cf2e57c4abc1a1b53531a18a570baeed39d8d7b65deb16b7e"}, + {file = "grpcio-1.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11d05402e0ac3a284443d8a432d3dfc76a6bd3f7b5858cddd75617af2d7bd9b"}, + {file = "grpcio-1.43.0-cp310-cp310-win32.whl", hash = "sha256:c36f418c925a41fccada8f7ae9a3d3e227bfa837ddbfddd3d8b0ac252d12dda9"}, + {file = "grpcio-1.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:772b943f34374744f70236bbbe0afe413ed80f9ae6303503f85e2b421d4bca92"}, + {file = "grpcio-1.43.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:cbc9b83211d905859dcf234ad39d7193ff0f05bfc3269c364fb0d114ee71de59"}, + {file = "grpcio-1.43.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:fb7229fa2a201a0c377ff3283174ec966da8f9fd7ffcc9a92f162d2e7fc9025b"}, + {file = "grpcio-1.43.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:17b75f220ee6923338155b4fcef4c38802b9a57bc57d112c9599a13a03e99f8d"}, + {file = "grpcio-1.43.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:6620a5b751b099b3b25553cfc03dfcd873cda06f9bb2ff7e9948ac7090e20f05"}, + {file = "grpcio-1.43.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:1898f999383baac5fcdbdef8ea5b1ef204f38dc211014eb6977ac6e55944d738"}, + {file = "grpcio-1.43.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47b6821238d8978014d23b1132713dac6c2d72cbb561cf257608b1673894f90a"}, + {file = "grpcio-1.43.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80398e9fb598060fa41050d1220f5a2440fe74ff082c36dda41ac3215ebb5ddd"}, + {file = "grpcio-1.43.0-cp36-cp36m-win32.whl", hash = "sha256:0110310eff07bb69782f53b7a947490268c4645de559034c43c0a635612e250f"}, + {file = "grpcio-1.43.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45401d00f2ee46bde75618bf33e9df960daa7980e6e0e7328047191918c98504"}, + {file = "grpcio-1.43.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:af78ac55933811e6a25141336b1f2d5e0659c2f568d44d20539b273792563ca7"}, + {file = "grpcio-1.43.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8b2b9dc4d7897566723b77422e11c009a0ebd397966b165b21b89a62891a9fdf"}, + {file = "grpcio-1.43.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:77ef653f966934b3bfdd00e4f2064b68880eb40cf09b0b99edfa5ee22a44f559"}, + {file = "grpcio-1.43.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e95b5d62ec26d0cd0b90c202d73e7cb927c369c3358e027225239a4e354967dc"}, + {file = "grpcio-1.43.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:04239e8f71db832c26bbbedb4537b37550a39d77681d748ab4678e58dd6455d6"}, + {file = "grpcio-1.43.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b4a7152187a49767a47d1413edde2304c96f41f7bc92cc512e230dfd0fba095"}, + {file = "grpcio-1.43.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8cc936a29c65ab39714e1ba67a694c41218f98b6e2a64efb83f04d9abc4386b"}, + {file = "grpcio-1.43.0-cp37-cp37m-win32.whl", hash = "sha256:577e024c8dd5f27cd98ba850bc4e890f07d4b5942e5bc059a3d88843a2f48f66"}, + {file = "grpcio-1.43.0-cp37-cp37m-win_amd64.whl", hash = "sha256:138f57e3445d4a48d9a8a5af1538fdaafaa50a0a3c243f281d8df0edf221dc02"}, + {file = "grpcio-1.43.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:08cf25f2936629db062aeddbb594bd76b3383ab0ede75ef0461a3b0bc3a2c150"}, + {file = "grpcio-1.43.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:01f4b887ed703fe82ebe613e1d2dadea517891725e17e7a6134dcd00352bd28c"}, + {file = "grpcio-1.43.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0aa8285f284338eb68962fe1a830291db06f366ea12f213399b520c062b01f65"}, + {file = "grpcio-1.43.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:0edbfeb6729aa9da33ce7e28fb7703b3754934115454ae45e8cc1db601756fd3"}, + {file = "grpcio-1.43.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:c354017819201053d65212befd1dcb65c2d91b704d8977e696bae79c47cd2f82"}, + {file = "grpcio-1.43.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50cfb7e1067ee5e00b8ab100a6b7ea322d37ec6672c0455106520b5891c4b5f5"}, + {file = "grpcio-1.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57f1aeb65ed17dfb2f6cd717cc109910fe395133af7257a9c729c0b9604eac10"}, + {file = "grpcio-1.43.0-cp38-cp38-win32.whl", hash = "sha256:fa26a8bbb3fe57845acb1329ff700d5c7eaf06414c3e15f4cb8923f3a466ef64"}, + {file = "grpcio-1.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:ade8b79a6b6aea68adb9d4bfeba5d647667d842202c5d8f3ba37ac1dc8e5c09c"}, + {file = "grpcio-1.43.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:124e718faf96fe44c98b05f3f475076be8b5198bb4c52a13208acf88a8548ba9"}, + {file = "grpcio-1.43.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2f96142d0abc91290a63ba203f01649e498302b1b6007c67bad17f823ecde0cf"}, + {file = "grpcio-1.43.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:31e6e489ccd8f08884b9349a39610982df48535881ec34f05a11c6e6b6ebf9d0"}, + {file = "grpcio-1.43.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:0e731f660e1e68238f56f4ce11156f02fd06dc58bc7834778d42c0081d4ef5ad"}, + {file = "grpcio-1.43.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:1f16725a320460435a8a5339d8b06c4e00d307ab5ad56746af2e22b5f9c50932"}, + {file = "grpcio-1.43.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b4543e13acb4806917d883d0f70f21ba93b29672ea81f4aaba14821aaf9bb0"}, + {file = "grpcio-1.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:594aaa0469f4fca7773e80d8c27bf1298e7bbce5f6da0f084b07489a708f16ab"}, + {file = "grpcio-1.43.0-cp39-cp39-win32.whl", hash = "sha256:5449ae564349e7a738b8c38583c0aad954b0d5d1dd3cea68953bfc32eaee11e3"}, + {file = "grpcio-1.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:bdf41550815a831384d21a498b20597417fd31bd084deb17d31ceb39ad9acc79"}, + {file = "grpcio-1.43.0.tar.gz", hash = "sha256:735d9a437c262ab039d02defddcb9f8f545d7009ae61c0114e19dda3843febe5"}, ] grpcio-tools = [ - {file = "grpcio-tools-1.54.2.tar.gz", hash = "sha256:e11c2c2aee53f340992e8e4d6a59172cbbbd0193f1351de98c4f810a5041d5ca"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:2b96f5f17d3156058be247fd25b062b4768138665694c00b056659618b8fb418"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:11939c9a8a39bd4815c7e88cb2fee48e1948775b59dbb06de8fcae5991e84f9e"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:129de5579f95d6a55dde185f188b4cbe19d1e2f1471425431d9930c31d300d70"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4128c01cd6f5ea8f7c2db405dbfd8582cd967d36e6fa0952565436633b0e591"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5c7292dd899ad8fa09a2be96719648cee37b17909fe8c12007e3bff58ebee61"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5ef30c2dbc63c1e0a462423ca4f95001814d26ef4fe66208e53fcf220ea3b717"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4abfc1892380abe6cef381eab86f9350cbd703bfe5d834095aa66fd91c886b6d"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-win32.whl", hash = "sha256:9acf443dcf6f68fbea3b7fb519e1716e014db1a561939f5aecc4abda74e4015d"}, - {file = "grpcio_tools-1.54.2-cp310-cp310-win_amd64.whl", hash = "sha256:21b9d2dee80f3f77e4097252e7f0db89772335a7300b72ab3d2e5c280872b1db"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:7b24fbab9e7598518ce4549e066df00aab79c2bf9bedcdde23fb5ef6a3cf532f"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:7baa210c20f71a242d9ae0e02734628f6948e8bee3bf538647894af427d28800"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e3d0e5188ff8dbaddac2ee44731d36f09c4eccd3eac7328e547862c44f75cacd"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27671c68c7e0e3c5ff9967f5500799f65a04e7b153b8ce10243c87c43199039d"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39d8e8806b8857fb473ca6a9c7bd800b0673dfdb7283ff569af0345a222f32c"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8e4c5a48f7b2e8798ce381498ee7b9a83c65b87ae66ee5022387394e5eb51771"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f285f8ef3de422717a36bd372239ae778b8cc112ce780ca3c7fe266dadc49fb"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-win32.whl", hash = "sha256:0f952c8a5c47e9204fe8959f7e9add149e660f6579d67cf65024c32736d34caf"}, - {file = "grpcio_tools-1.54.2-cp311-cp311-win_amd64.whl", hash = "sha256:3237149beec39e897fd62cef4aa1e1cd9422d7a95661d24bd0a79200b167e730"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:0ab1b323905d449298523db5d34fa5bf5fffd645bd872b25598e2f8a01f0ea39"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:7d7e6e8d62967b3f037f952620cb7381cc39a4bd31790c75fcfba56cc975d70b"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:7f4624ef2e76a3a5313c4e61a81be38bcc16b59a68a85d30758b84cd2102b161"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e543f457935ba7b763b121f1bf893974393b4d30065042f947f85a8d81081b80"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0239b929eb8b3b30b2397eef3b9abb245087754d77c3721e3be43c44796de87d"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0de05c7698c655e9a240dc34ae91d6017b93143ac89e5b20046d7ca3bd09c27c"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3ce0b98fb581c471424d2cda45120f57658ed97677c6fec4d6decf5d7c1b976"}, - {file = "grpcio_tools-1.54.2-cp37-cp37m-win_amd64.whl", hash = "sha256:37393ef90674964175923afe3859fc5a208e1ece565f642b4f76a8c0224a0993"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:8e4531267736d88fde1022b36dd42ed8163e3575bcbd12bfed96662872aa93fe"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:a0b7049814442f918b522d66b1d015286afbeb9e6d141af54bbfafe31710a3c8"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b80585e06c4f0082327eb5c9ad96fbdb2b0e7c14971ea5099fe78c22f4608451"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39fd530cfdf58dc05125775cc233b05554d553d27478f14ae5fd8a6306f0cb28"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bb9ec4aea0f2b3006fb002fa59e5c10f92b48fc374619fbffd14d2b0e388c3e"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d512de051342a576bb89777476d13c5266d9334cf4badb6468aed9dc8f5bdec1"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1b8ee3099c51ce987fa8a08e6b93fc342b10228415dd96b5c0caa0387f636a6f"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-win32.whl", hash = "sha256:6037f123905dc0141f7c8383ca616ef0195e79cd3b4d82faaee789d4045e891b"}, - {file = "grpcio_tools-1.54.2-cp38-cp38-win_amd64.whl", hash = "sha256:10dd41862f579d185c60f629b5ee89103e216f63b576079d258d974d980bad87"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:f6787d07fdab31a32c433c1ba34883dea6559d8a3fbe08fb93d834ca34136b71"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:21b1467e31e44429d2a78b50135c9cdbd4b8f6d3b5cd548bc98985d3bdc352d0"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:30a49b8b168aced2a4ff40959e6c4383ad6cfd7a20839a47a215e9837eb722dc"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8742122782953d2fd038f0a199f047a24e941cc9718b1aac90876dbdb7167739"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503ef1351c62fb1d6747eaf74932b609d8fdd4345b3591ef910adef8fa9969d0"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:72d15de4c4b6a764a76c4ae69d99c35f7a0751223688c3f7e62dfa95eb4f61be"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df079479fb1b9e488334312e35ebbf30cbf5ecad6c56599f1a961800b33ab7c1"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-win32.whl", hash = "sha256:49c2846dcc4803476e839d8bd4db8845e928f19130e0ea86121f2d1f43d2b452"}, - {file = "grpcio_tools-1.54.2-cp39-cp39-win_amd64.whl", hash = "sha256:b82ca472db9c914c44e39a41e9e8bd3ed724523dd7aff5ce37592b8d16920ed9"}, + {file = "grpcio-tools-1.43.0.tar.gz", hash = "sha256:f42f1d713096808b1b0472dd2a3749b712d13f0092dab9442d9c096446e860b2"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:766771ef5b60ebcba0a3bdb302dd92fda988552eb8508451ff6d97371eac38e5"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:178a881db5de0f89abf3aeeb260ecfd1116cc31f88fb600a45fb5b19c3323b33"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:019f55929e963214471825c7a4cdab7a57069109d5621b24e4db7b428b5fe47d"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6c0e1d1b47554c580882d392b739df91a55b6a8ec696b2b2e1bbc127d63df2c"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c5c80098fa69593b828d119973744de03c3f9a6935df8a02e4329a39b7072f5"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-win32.whl", hash = "sha256:53f7dcaa4218df1b64b39d0fc7236a8270e8ab2db4ab8cd1d2fda0e6d4544946"}, + {file = "grpcio_tools-1.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:5be6d402b0cafef20ba3abb3baa37444961d9a9c4a6434d3d7c1f082f7697deb"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:8953fdebef6905d7ff13a5a376b21b6fecd808d18bf4f0d3990ffe4a215d56eb"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:18870dcc8369ac4c37213e6796d8dc20494ea770670204f5e573f88e69eaaf0b"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:010a4be6a2fccbd6741a4809c5da7f2e39a1e9e227745e6b495be567638bbeb9"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:426f16b6b14d533ce61249a18fbcd1a23a4fa0c71a6d7ab347b1c7f862847bb8"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:f974cb0bea88bac892c3ed16da92c6ac88cff0fea17f24bf0e1892eb4d27cd00"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55c2e604536e06248e2f81e549737fb3a180c8117832e494a0a8a81fbde44837"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f97f9ffa49348fb24692751d2d4455ef2968bd07fe536d65597caaec14222629"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-win32.whl", hash = "sha256:6eaf97414237b8670ae9fa623879a26eabcc4c635b550c79a81e17eb600d6ae3"}, + {file = "grpcio_tools-1.43.0-cp36-cp36m-win_amd64.whl", hash = "sha256:04f100c1f6a7c72c537760c33582f6970070bd6fa6676b529bccfa31cc58bc79"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:9dbb6d1f58f26d88ae689f1b49de84cfaf4786c81c01b9001d3ceea178116a07"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:63862a441a77f6326ea9fe4bb005882f0e363441a5968d9cf8621c34d3dadc2b"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6dea0cb2e79b67593553ed8662f70e4310599fa8850fc0e056b19fcb63572b7f"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3eb4aa5b0e578c3d9d9da8e37a2ef73654287a498b8081543acd0db7f0ec1a9c"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:09464c6b17663088144b7e6ea10e9465efdcee03d4b2ffefab39a799bd8360f8"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2458d6b0404f83d95aef00cec01f310d30e9719564a25be50e39b259f6a2da5d"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e9bb5da437364b7dcd2d3c6850747081ecbec0ba645c96c6d471f7e21fdcadb"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-win32.whl", hash = "sha256:2737f749a6ab965748629e619b35f3e1cbe5820fc79e34c88f73cb99efc71dde"}, + {file = "grpcio_tools-1.43.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c39cbe7b902bb92f9afaa035091f5e2b8be35acbac501fec8cb6a0be7d7cdbbd"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:05550ba473cff7c09e905fcfb2263fd1f7600389660194ec022b5d5a3802534b"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:ce13a922db8f5f95c5041d3a4cbf04d942b353f0cba9b251a674f69a31a2d3a6"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:f19d40690c97365c1c1bde81474e6f496d7ab76f87e6d2889c72ad01bac98f2d"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba3da574eb08fcaed541b3fc97ce217360fd86d954fa9ad6a604803d57a2e049"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:efd1eb5880001f5189cfa3a774675cc9bbc8cc51586a3e90fe796394ac8626b8"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:234c7a5af653357df5c616e013173eddda6193146c8ab38f3108c4784f66be26"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7e3662f62d410b3f81823b5fa0f79c6e0e250977a1058e4131867b85138a661"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-win32.whl", hash = "sha256:5f2e584d7644ef924e9e042fa151a3bb9f7c28ef1ae260ee6c9cb327982b5e94"}, + {file = "grpcio_tools-1.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:98dcb5b756855110fb661ccd6a93a716610b7efcd5720a3aec01358a1a892c30"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:61ef6cb6ccf9b9c27bb85fffc5338194bcf444df502196c2ad0ff8df4706d41e"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:1def9b68ac9e62674929bc6590a33d89635f1cf16016657d9e16a69f41aa5c36"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:b68cc0c95a0f8c757e8d69b5fa46111d5c9d887ae62af28f827649b1d1b70fe1"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e956b5c3b586d7b27eae49fb06f544a26288596fe12e22ffec768109717276d1"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:671e61bbc91d8d568f12c3654bb5a91fce9f3fdfd5ec2cfc60c2d3a840449aa6"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7173ed19854d1066bce9bdc09f735ca9c13e74a25d47a1cc5d1fe803b53bffb"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1adb0dbcc1c10b86dcda910b8f56e39210e401bcee923dba166ba923a5f4696a"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-win32.whl", hash = "sha256:ebfb94ddb454a6dc3a505d9531dc81c948e6364e181b8795bfad3f3f479974dc"}, + {file = "grpcio_tools-1.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:d21928b680e6e29538688cffbf53f3d5a53cff0ec8f0c33139641700045bdf1a"}, ] identify = [ - {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, - {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, + {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"}, + {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"}, ] -iniconfig = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +importlib-metadata = [ + {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, + {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, +] +importlib-resources = [ + {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, + {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, ] invoke = [ - {file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"}, - {file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"}, + {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, + {file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"}, + {file = "invoke-1.4.1.tar.gz", hash = "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"}, ] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] lxml = [ - {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, - {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, - {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, - {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, - {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, - {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, - {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, - {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, - {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, - {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, - {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, - {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, - {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, - {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, - {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, - {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, - {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, - {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, - {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, - {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, - {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, - {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, - {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, - {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, - {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, - {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, - {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, - {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, - {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, - {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, - {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, - {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, - {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, - {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, - {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, - {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, - {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, - {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, - {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, - {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, - {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, + {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, + {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, + {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, + {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, + {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, + {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, + {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, + {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, + {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, + {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, + {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, + {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, + {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, + {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, + {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, + {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, + {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, + {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, + {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, + {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, + {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, + {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, + {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, + {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, + {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, + {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, ] -Mako = [ - {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, - {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, +mako = [ + {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, + {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, ] -MarkupSafe = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -997,140 +1017,120 @@ mock = [ {file = "mock-4.0.2-py3-none-any.whl", hash = "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0"}, {file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"}, ] -mypy-extensions = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +more-itertools = [ + {file = "more-itertools-8.13.0.tar.gz", hash = "sha256:a42901a0a5b169d925f6f217cd5a190e32ef54360905b9c39ee7db5313bfec0f"}, + {file = "more_itertools-8.13.0-py3-none-any.whl", hash = "sha256:c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb"}, ] netaddr = [ {file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"}, {file = "netaddr-0.7.19.tar.gz", hash = "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"}, ] nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] packaging = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-3.0.0-py3-none-any.whl", hash = "sha256:6bef55b882c9d130f8015b9a26f4bd93f710e90fe7478b9dcc810304e79b3cd8"}, - {file = "paramiko-3.0.0.tar.gz", hash = "sha256:fedc9b1dd43bc1d45f67f1ceca10bc336605427a46dcdf8dec6bfea3edf57965"}, + {file = "paramiko-2.10.4-py2.py3-none-any.whl", hash = "sha256:3c9ed6084f4b671ab66dc3c729092d32d96c3258f1426071301cb33654b09027"}, + {file = "paramiko-2.10.4.tar.gz", hash = "sha256:3d2e650b6812ce6d160abff701d6ef4434ec97934b13e95cf1ad3da70ffb5c58"}, ] -pathlib2 = [ - {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, - {file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"}, -] -pathspec = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, -] -Pillow = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, +pillow = [ + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] platformdirs = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] pre-commit = [ {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"}, {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, ] protobuf = [ - {file = "protobuf-4.21.9-cp310-abi3-win32.whl", hash = "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392"}, - {file = "protobuf-4.21.9-cp310-abi3-win_amd64.whl", hash = "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf"}, - {file = "protobuf-4.21.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1"}, - {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719"}, - {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740"}, - {file = "protobuf-4.21.9-cp37-cp37m-win32.whl", hash = "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c"}, - {file = "protobuf-4.21.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536"}, - {file = "protobuf-4.21.9-cp38-cp38-win32.whl", hash = "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce"}, - {file = "protobuf-4.21.9-cp38-cp38-win_amd64.whl", hash = "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444"}, - {file = "protobuf-4.21.9-cp39-cp39-win32.whl", hash = "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"}, - {file = "protobuf-4.21.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b"}, - {file = "protobuf-4.21.9-py2.py3-none-any.whl", hash = "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965"}, - {file = "protobuf-4.21.9-py3-none-any.whl", hash = "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b"}, - {file = "protobuf-4.21.9.tar.gz", hash = "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99"}, + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1148,7 +1148,7 @@ pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] -PyNaCl = [ +pynacl = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, @@ -1160,82 +1160,63 @@ PyNaCl = [ {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, ] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] pyproj = [ - {file = "pyproj-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:473961faef7a9fd723c5d432f65220ea6ab3854e606bf84b4d409a75a4261c78"}, - {file = "pyproj-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07c9d8d7ec009bbac09e233cfc725601586fe06880e5538a3a44eaf560ba3a62"}, - {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fef9c1e339f25c57f6ae0558b5ab1bbdf7994529a30d8d7504fc6302ea51c03"}, - {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:140fa649fedd04f680a39f8ad339799a55cb1c49f6a84e1b32b97e49646647aa"}, - {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b59c08aea13ee428cf8a919212d55c036cc94784805ed77c8f31a4d1f541058c"}, - {file = "pyproj-3.3.1-cp310-cp310-win32.whl", hash = "sha256:1adc9ccd1bf04998493b6a2e87e60656c75ab790653b36cfe351e9ef214828ed"}, - {file = "pyproj-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:42eea10afc750fccd1c5c4ba56de29ab791ab4d83c1f7db72705566282ac5396"}, - {file = "pyproj-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:531ea36519fa7b581466d4b6ab32f66ae4dadd9499d726352f71ee5e19c3d1c5"}, - {file = "pyproj-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67025e37598a6bbed2c9c6c9e4c911f6dd39315d3e1148ead935a5c4d64309d5"}, - {file = "pyproj-3.3.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aed1a3c0cd4182425f91b48d5db39f459bc2fe0d88017ead6425a1bc85faee33"}, - {file = "pyproj-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cc4771403db54494e1e55bca8e6d33cde322f8cf0ed39f1557ff109c66d2cd1"}, - {file = "pyproj-3.3.1-cp38-cp38-win32.whl", hash = "sha256:c99f7b5757a28040a2dd4a28c9805fdf13eef79a796f4a566ab5cb362d10630d"}, - {file = "pyproj-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:5dac03d4338a4c8bd0f69144c527474f517b4cbd7d2d8c532cd8937799723248"}, - {file = "pyproj-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56b0f9ee2c5b2520b18db30a393a7b86130cf527ddbb8c96e7f3c837474a9d79"}, - {file = "pyproj-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1032e5dfb50eae06382bcc7b9011b994f7104d932fe91bd83a722275e30e8ce"}, - {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f92d8f6514516124abb714dce912b20867831162cfff9fae2678ef07b6fcf0f"}, - {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ef1bfbe2dcc558c7a98e2f1836abdcd630390f3160724a6f4f5c818b2be0ad5"}, - {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca5f32b56210429b367ca4f9a57ffe67975c487af82e179a24370879a3daf68"}, - {file = "pyproj-3.3.1-cp39-cp39-win32.whl", hash = "sha256:aba199704c824fb84ab64927e7bc9ef71e603e483130ec0f7e09e97259b8f61f"}, - {file = "pyproj-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:120d45ed73144c65e9677dc73ba8a531c495d179dd9f9f0471ac5acc02d7ac4b"}, - {file = "pyproj-3.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:52efb681647dfac185cc655a709bc0caaf910031a0390f816f5fc8ce150cbedc"}, - {file = "pyproj-3.3.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ab0d6e38fda7c13726afacaf62e9f9dd858089d67910471758afd9cb24e0ecd"}, - {file = "pyproj-3.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45487942c19c5a8b09c91964ea3201f4e094518e34743cae373889a36e3d9260"}, - {file = "pyproj-3.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:797ad5655d484feac14b0fbb4a4efeaac0cf780a223046e2465494c767fd1c3b"}, - {file = "pyproj-3.3.1.tar.gz", hash = "sha256:b3d8e14d91cc95fb3dbc03a9d0588ac58326803eefa5bbb0978d109de3304fbe"}, + {file = "pyproj-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f942a976ea3de6a519cf48be30a12f465e44d0ac0c38a0d820ab3acfcc0a48a6"}, + {file = "pyproj-3.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:09db64a8088b23f001e574d92bcc3080bf7de44ddca152d0282a2b50c918a64a"}, + {file = "pyproj-3.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:cba99e171d744969e13a865ad28fa9c949c4400b0e9c431a802cdd804f52f632"}, + {file = "pyproj-3.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:81c06df20d09d621e52791c19ce3c880695fb430061e59c2472fa5467e890391"}, + {file = "pyproj-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:3e7e851e6d58c16ac2cd920a1bacb7fbb24758a6fcd7f234d594a88ebae04ec9"}, + {file = "pyproj-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:aa0a2981b25145523ca17a643c5be077fe13e514fdca9b6d1c412a95d723a5a5"}, + {file = "pyproj-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:708d6e01b9ff3d6dc62a5ad2d2ba1264a863eaa657c1a9bf713a10cc35d34553"}, + {file = "pyproj-3.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36ba436675f9dea4ab3db7d9a32d3ff11c2fbb4d6690a83454d2f3c5c0b54041"}, + {file = "pyproj-3.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:489a96da87d8846c34c90da90e637544e4f4f50a13589b5aac54297f5ee1b01d"}, + {file = "pyproj-3.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4a333f3e46fe8b2eb4647a3daa3a2cec52ddc6c107c653b45880526114942ee8"}, + {file = "pyproj-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:9e2ef75401f17062166d3fe53c555cd62c9577697a2f5ded916b23c54e5db497"}, + {file = "pyproj-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7bfaa34e8bb0510d4380310374deecd9e4328b9cf556925cfb45b5a94d5bbdbe"}, + {file = "pyproj-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9666d01faf4e758ac68f2c16695c90de49c3170e3760988bf76a34aae11f4e15"}, + {file = "pyproj-3.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c658afc8a6115b58b02aa53d27bf2a67c1b00b55067edb1b7711c6c7391cfaa9"}, + {file = "pyproj-3.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fee7517bd389a1db7b8bebb18838d04dedca9eaacda01d353d98f5ee421f263e"}, + {file = "pyproj-3.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:86ef2fcd584a3222bf73e2befc24b2badd139b3371f4a1e88649978ef7649540"}, + {file = "pyproj-3.0.1-cp38-cp38-win32.whl", hash = "sha256:d27d40ec541ef69a5107bfcd85f40170e9e122ceb6315ce508cd44d199983d41"}, + {file = "pyproj-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:bc70b6adcfa713d89bc561673cb57af5fb3a1718cd7d57ec537430cd1007a864"}, + {file = "pyproj-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b845510255f9580d7e226dd3321a51c468cefb7be24e46415caf67caa4287c4"}, + {file = "pyproj-3.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7ae8e7052f18fde1884574da449010e94fa205ad27aeeaa34a097f49a1ed6a2b"}, + {file = "pyproj-3.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:a3805e026a5547be205a5e322c08e3069f0a48c63bbd53dbc7a8e3499bc66d58"}, + {file = "pyproj-3.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1be7d54900eb7e2d1e637319080b3a047c70d1fb2f3c12d3400c0fa8a90cf440"}, + {file = "pyproj-3.0.1-cp39-cp39-win32.whl", hash = "sha256:09bead60769e69b592e8cb3ac51b5215f75e9bb9c213ce575031961deb48d6da"}, + {file = "pyproj-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:a3a8ab19232bf4f4bb2590536538881b7bd0c07df23e0c2a792402ca2476c197"}, + {file = "pyproj-3.0.1.tar.gz", hash = "sha256:bfbac35490dd17f706700673506eeb8170f8a2a63fb5878171d4e6eef242d141"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] -PyYAML = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] -setuptools = [ - {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, - {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, +pyyaml = [ + {file = "PyYAML-5.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f7a21e3d99aa3095ef0553e7ceba36fb693998fbb1226f1392ce33681047465f"}, + {file = "PyYAML-5.4-cp27-cp27m-win32.whl", hash = "sha256:52bf0930903818e600ae6c2901f748bc4869c0c406056f679ab9614e5d21a166"}, + {file = "PyYAML-5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:a36a48a51e5471513a5aea920cdad84cbd56d70a5057cca3499a637496ea379c"}, + {file = "PyYAML-5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5e7ac4e0e79a53451dc2814f6876c2fa6f71452de1498bbe29c0b54b69a986f4"}, + {file = "PyYAML-5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc552b6434b90d9dbed6a4f13339625dc466fd82597119897e9489c953acbc22"}, + {file = "PyYAML-5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0dc9f2eb2e3c97640928dec63fd8dc1dd91e6b6ed236bd5ac00332b99b5c2ff9"}, + {file = "PyYAML-5.4-cp36-cp36m-win32.whl", hash = "sha256:5a3f345acff76cad4aa9cb171ee76c590f37394186325d53d1aa25318b0d4a09"}, + {file = "PyYAML-5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:f3790156c606299ff499ec44db422f66f05a7363b39eb9d5b064f17bd7d7c47b"}, + {file = "PyYAML-5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:124fd7c7bc1e95b1eafc60825f2daf67c73ce7b33f1194731240d24b0d1bf628"}, + {file = "PyYAML-5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8b818b6c5a920cbe4203b5a6b14256f0e5244338244560da89b7b0f1313ea4b6"}, + {file = "PyYAML-5.4-cp37-cp37m-win32.whl", hash = "sha256:737bd70e454a284d456aa1fa71a0b429dd527bcbf52c5c33f7c8eee81ac16b89"}, + {file = "PyYAML-5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:7242790ab6c20316b8e7bb545be48d7ed36e26bbe279fd56f2c4a12510e60b4b"}, + {file = "PyYAML-5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cc547d3ead3754712223abb7b403f0a184e4c3eae18c9bb7fd15adef1597cc4b"}, + {file = "PyYAML-5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8635d53223b1f561b081ff4adecb828fd484b8efffe542edcfdff471997f7c39"}, + {file = "PyYAML-5.4-cp38-cp38-win32.whl", hash = "sha256:26fcb33776857f4072601502d93e1a619f166c9c00befb52826e7b774efaa9db"}, + {file = "PyYAML-5.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2243dd033fd02c01212ad5c601dafb44fbb293065f430b0d3dbf03f3254d615"}, + {file = "PyYAML-5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:31ba07c54ef4a897758563e3a0fcc60077698df10180abe4b8165d9895c00ebf"}, + {file = "PyYAML-5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:02c78d77281d8f8d07a255e57abdbf43b02257f59f50cc6b636937d68efa5dd0"}, + {file = "PyYAML-5.4-cp39-cp39-win32.whl", hash = "sha256:fdc6b2cb4b19e431994f25a9160695cc59a4e861710cc6fc97161c5e845fc579"}, + {file = "PyYAML-5.4-cp39-cp39-win_amd64.whl", hash = "sha256:8bf38641b4713d77da19e91f8b5296b832e4db87338d6aeffe422d42f1ca896d"}, + {file = "PyYAML-5.4.tar.gz", hash = "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1245,15 +1226,19 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] virtualenv = [ - {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, - {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto index 25be616d..28e00bcb 100644 --- a/daemon/proto/core/api/grpc/configservices.proto +++ b/daemon/proto/core/api/grpc/configservices.proto @@ -41,8 +41,6 @@ message ConfigMode { message GetConfigServiceDefaultsRequest { string name = 1; - int32 session_id = 2; - int32 node_id = 3; } message GetConfigServiceDefaultsResponse { diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 09f2c764..4969e9c9 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -545,8 +545,6 @@ message NodeType { CONTROL_NET = 13; DOCKER = 15; LXC = 16; - WIRELESS = 17; - PODMAN = 18; } } @@ -670,8 +668,6 @@ message Interface { int32 mtu = 10; int32 node_id = 11; int32 net2_id = 12; - int32 nem_id = 13; - int32 nem_port = 14; } message SessionLocation { diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 0d1acf7a..70d684dd 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "9.0.3" +version = "9.0.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" @@ -24,26 +24,27 @@ core-service-update = "core.scripts.serviceupdate:main" core-cleanup = "core.scripts.cleanup:main" [tool.poetry.dependencies] -python = "^3.9" -fabric = "2.7.1" -grpcio = "1.54.2" -invoke = "1.7.3" -lxml = "4.9.1" +python = "^3.6" +dataclasses = { version = "*", python = "~3.6" } +fabric = "2.5.0" +grpcio = "1.43.0" +invoke = "1.4.1" +lxml = "4.6.5" +mako = "1.1.3" netaddr = "0.7.19" -protobuf = "4.21.9" -pyproj = "3.3.1" -Pillow = "9.4.0" -Mako = "1.2.3" -PyYAML = "6.0.1" +pillow = "8.3.2" +protobuf = "3.19.4" +pyproj = "3.0.1" +pyyaml = "5.4" -[tool.poetry.group.dev.dependencies] -pytest = "6.2.5" -grpcio-tools = "1.54.2" -black = "22.12.0" +[tool.poetry.dev-dependencies] +black = "==19.3b0" flake8 = "3.8.2" +grpcio-tools = "1.43.0" isort = "4.3.21" mock = "4.0.2" pre-commit = "2.1.1" +pytest = "5.4.3" [tool.isort] skip_glob = "*_pb2*.py,doc,build" diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index b668fb07..4827ed86 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -59,6 +59,8 @@ def patcher(request): LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" ) patch_manager.patch_obj(CoreNode, "create_file") + patch_manager.patch_obj(Session, "write_state") + patch_manager.patch_obj(Session, "write_nodes") yield patch_manager patch_manager.shutdown() diff --git a/daemon/tests/test_utils.py b/daemon/tests/test_utils.py index 21d092ac..5a4f25a4 100644 --- a/daemon/tests/test_utils.py +++ b/daemon/tests/test_utils.py @@ -9,7 +9,7 @@ class TestUtils: no_args = "()" one_arg = "('one',)" two_args = "('one', 'two')" - unicode_args = "('one', 'two', 'three')" + unicode_args = u"('one', 'two', 'three')" # when no_args = utils.make_tuple_fromstr(no_args, str) diff --git a/dockerfiles/Dockerfile.centos b/dockerfiles/Dockerfile.centos deleted file mode 100644 index 06654486..00000000 --- a/dockerfiles/Dockerfile.centos +++ /dev/null @@ -1,78 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM centos:7 -LABEL Description="CORE Docker CentOS Image" - -ARG PREFIX=/usr -ARG BRANCH=master -ENV LANG en_US.UTF-8 -ARG PROTOC_VERSION=3.19.6 -ARG VENV_PATH=/opt/core/venv -ENV PATH="$PATH:${VENV_PATH}/bin" -WORKDIR /opt - -# install system dependencies -RUN yum -y update && \ - yum install -y \ - xterm \ - git \ - sudo \ - wget \ - tzdata \ - unzip \ - libpcap-devel \ - libpcre3-devel \ - libxml2-devel \ - protobuf-devel \ - unzip \ - uuid-devel \ - tcpdump \ - make && \ - yum-builddep -y python3 && \ - yum autoremove -y && \ - yum install -y hostname - -# install python3.9 -RUN wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz && \ - tar xf Python-3.9.15.tgz && \ - cd Python-3.9.15 && \ - ./configure --enable-optimizations --with-ensurepip=install && \ - make -j$(nproc) altinstall && \ - python3.9 -m pip install --upgrade pip && \ - cd /opt && \ - rm -rf Python-3.9.15 - -# install core -RUN git clone https://github.com/coreemu/core && \ - cd core && \ - git checkout ${BRANCH} && \ - NO_SYSTEM=1 PYTHON=/usr/local/bin/python3.9 ./setup.sh && \ - PATH=/root/.local/bin:$PATH PYTHON=/usr/local/bin/python3.9 inv install -v -p ${PREFIX} --no-python - -# install emane -RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - tar xf emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - cd emane-1.3.3-release-1/rpms/el7/x86_64 && \ - yum install -y epel-release && \ - yum install -y ./openstatistic*.rpm ./emane*.rpm ./python3-emane_*.rpm && \ - cd ../../../.. && \ - rm emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - rm -rf emane-1.3.3-release-1 - -# install emane python bindings -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 && \ - git checkout v1.3.3 && \ - ./autogen.sh && \ - PYTHON=${VENV_PATH}/bin/python ./configure --prefix=/usr && \ - cd src/python && \ - 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 diff --git a/dockerfiles/Dockerfile.centos-package b/dockerfiles/Dockerfile.centos-package deleted file mode 100644 index 8d4a1296..00000000 --- a/dockerfiles/Dockerfile.centos-package +++ /dev/null @@ -1,89 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM centos:7 -LABEL Description="CORE CentOS Image" - -ENV LANG en_US.UTF-8 -ARG PROTOC_VERSION=3.19.6 -ARG VENV_PATH=/opt/core/venv -ENV PATH="$PATH:${VENV_PATH}/bin" -WORKDIR /opt - -# install basic dependencies -RUN yum -y update && \ - yum install -y \ - xterm \ - git \ - sudo \ - wget \ - tzdata \ - unzip \ - libpcap-devel \ - libpcre3-devel \ - libxml2-devel \ - protobuf-devel \ - unzip \ - uuid-devel \ - tcpdump \ - automake \ - gawk \ - libreadline-devel \ - libtool \ - pkg-config \ - make && \ - yum-builddep -y python3 && \ - yum autoremove -y && \ - yum install -y hostname - -# install python3.9 -RUN wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz && \ - tar xf Python-3.9.15.tgz && \ - cd Python-3.9.15 && \ - ./configure --enable-optimizations --with-ensurepip=install && \ - make -j$(nproc) altinstall && \ - python3.9 -m pip install --upgrade pip && \ - cd /opt && \ - rm -rf Python-3.9.15 - -# install core -COPY core_*.rpm . -RUN PYTHON=/usr/local/bin/python3.9 yum install -y ./core_*.rpm && \ - rm -f core_*.rpm - -# install ospf mdr -RUN git clone https://github.com/USNavalResearchLaboratory/ospf-mdr.git && \ - cd ospf-mdr && \ - ./bootstrap.sh && \ - ./configure --disable-doc --enable-user=root --enable-group=root \ - --with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga && \ - make -j$(nproc) && \ - make install && \ - cd /opt && \ - rm -rf ospf-mdr - - # install emane -RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - tar xf emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - cd emane-1.3.3-release-1/rpms/el7/x86_64 && \ - yum install -y epel-release && \ - yum install -y ./openstatistic*.rpm ./emane*.rpm ./python3-emane_*.rpm && \ - cd ../../../.. && \ - rm emane-1.3.3-release-1.el7.x86_64.tar.gz && \ - rm -rf emane-1.3.3-release-1 - -# install emane python bindings -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 && \ - git checkout v1.3.3 && \ - ./autogen.sh && \ - PYTHON=${VENV_PATH}/bin/python ./configure --prefix=/usr && \ - cd src/python && \ - 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 diff --git a/dockerfiles/Dockerfile.ubuntu-package b/dockerfiles/Dockerfile.ubuntu-package deleted file mode 100644 index b8f66165..00000000 --- a/dockerfiles/Dockerfile.ubuntu-package +++ /dev/null @@ -1,75 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM ubuntu:22.04 -LABEL Description="CORE Docker Ubuntu Image" - -ENV DEBIAN_FRONTEND=noninteractive -ARG PROTOC_VERSION=3.19.6 -ARG VENV_PATH=/opt/core/venv -ENV PATH="$PATH:${VENV_PATH}/bin" -WORKDIR /opt - -# install basic dependencies -RUN apt-get update -y && \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - python3 \ - python3-tk \ - python3-pip \ - python3-venv \ - libpcap-dev \ - libpcre3-dev \ - libprotobuf-dev \ - libxml2-dev \ - protobuf-compiler \ - unzip \ - uuid-dev \ - automake \ - gawk \ - git \ - wget \ - libreadline-dev \ - libtool \ - pkg-config \ - g++ \ - make \ - iputils-ping \ - tcpdump && \ - apt-get autoremove -y - -# install core -COPY core_*.deb . -RUN apt-get install -y ./core_*.deb && \ - rm -f core_*.deb - -# install ospf mdr -RUN git clone https://github.com/USNavalResearchLaboratory/ospf-mdr.git && \ - cd ospf-mdr && \ - ./bootstrap.sh && \ - ./configure --disable-doc --enable-user=root --enable-group=root \ - --with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga && \ - make -j$(nproc) && \ - make install && \ - 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 diff --git a/docs/architecture.md b/docs/architecture.md index b9c5c91c..410b37ac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,22 +1,25 @@ # CORE Architecture +* Table of Contents +{:toc} + ## Main Components * core-daemon - * Manages emulated sessions of nodes and links for a given network - * Nodes are created using Linux namespaces - * Links are created using Linux bridges and virtual ethernet peers - * Packets sent over links are manipulated using traffic control - * Provides gRPC API + * Manages emulated sessions of nodes and links for a given network + * Nodes are created using Linux namespaces + * Links are created using Linux bridges and virtual ethernet peers + * Packets sent over links are manipulated using traffic control + * Provides gRPC API * core-gui - * GUI and daemon communicate over gRPC API - * Drag and drop creation for nodes and links - * Can launch terminals for emulated nodes in running sessions - * Can save/open scenario files to recreate previous sessions + * GUI and daemon communicate over gRPC API + * Drag and drop creation for nodes and links + * Can launch terminals for emulated nodes in running sessions + * Can save/open scenario files to recreate previous sessions * vnoded - * Command line utility for creating CORE node namespaces + * Command line utility for creating CORE node namespaces * vcmd - * Command line utility for sending shell commands to nodes + * Command line utility for sending shell commands to nodes ![](static/architecture.png) @@ -54,5 +57,5 @@ rules. CORE has been released by Boeing to the open source community under the BSD license. If you find CORE useful for your work, please contribute back to the project. Contributions can be as simple as reporting a bug, dropping a line of -encouragement, or can also include submitting patches or maintaining aspects +encouragement, or can also include submitting patches or maintaining aspects of the tool. diff --git a/docs/configservices.md b/docs/configservices.md index da81aa48..f0fa7bdd 100644 --- a/docs/configservices.md +++ b/docs/configservices.md @@ -1,4 +1,7 @@ -# Config Services +# CORE Config Services + +* Table of Contents +{:toc} ## Overview @@ -12,7 +15,6 @@ CORE services are a convenience for creating reusable dynamic scripts to run on nodes, for carrying out specific task(s). This boilds down to the following functions: - * generating files the service will use, either directly for commands or for configuration * command(s) for starting a service * command(s) for validating a service @@ -79,28 +81,30 @@ introduced to automate tasks. ### Creating New Services -!!! note - - The directory base name used in **custom_services_dir** below should - be unique and should not correspond to any existing Python module name. - For example, don't use the name **subprocess** or **services**. - 1. Modify the example service shown below to do what you want. It could generate config/script files, mount per-node directories, start processes/scripts, etc. Your file can define one or more classes to be imported. You can create multiple Python files that will be imported. -2. Put these files in a directory such as **~/.coregui/custom_services**. +2. Put these files in a directory such as ~/.coregui/custom_services + Note that the last component of this directory name **myservices** should not + be named something like **services** which conflicts with an existing module. 3. Add a **custom_config_services_dir = ~/.coregui/custom_services** entry to the /etc/core/core.conf file. + **NOTE:** + The directory name used in **custom_services_dir** should be unique and + should not correspond to + any existing Python module name. For example, don't use the name **subprocess** + or **services**. + 4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax) should be displayed in the terminal (or service log, like journalctl). 5. Start using your custom service on your nodes. You can create a new node type that uses your service, or change the default services for an existing - node type, or change individual nodes. + node type, or change individual nodes. . ### Example Custom Service @@ -117,7 +121,6 @@ from typing import Dict, List from core.config import ConfigString, ConfigBool, Configuration from core.configservice.base import ConfigService, ConfigServiceMode, ShadowDir - # class that subclasses ConfigService class ExampleService(ConfigService): # unique name for your service within CORE @@ -126,7 +129,7 @@ class ExampleService(ConfigService): group: str = "ExampleGroup" # directories that the service should shadow mount, hiding the system directory directories: List[str] = [ - "/usr/local/core", + "/usr/local/core", ] # files that this service should generate, defaults to nodes home directory # or can provide an absolute path to a mounted directory diff --git a/docs/ctrlnet.md b/docs/ctrlnet.md index d20e3a41..9ecc2e3f 100644 --- a/docs/ctrlnet.md +++ b/docs/ctrlnet.md @@ -1,10 +1,13 @@ # CORE Control Network +* Table of Contents +{:toc} + ## Overview The CORE control network allows the virtual nodes to communicate with their host environment. There are two types: the primary control network and -auxiliary control networks. The primary control network is used mainly for +auxiliary control networks. The primary control network is used mainly for communicating with the virtual nodes from host machines and for master-slave communications in a multi-server distributed environment. Auxiliary control networks have been introduced to for routing namespace hosted emulation @@ -27,19 +30,15 @@ new sessions will use by default. To simultaneously run multiple sessions with control networks, the session option should be used instead of the *core.conf* default. -!!! note +> **NOTE:** If you have a large scenario with more than 253 nodes, use a control +network prefix that allows more than the suggested */24*, such as */23* or +greater. - If you have a large scenario with more than 253 nodes, use a control - network prefix that allows more than the suggested */24*, such as */23* or - greater. - -!!! note - - Running a session with a control network can fail if a previous - session has set up a control network and the its bridge is still up. Close - the previous session first or wait for it to complete. If unable to, the - **core-daemon** may need to be restarted and the lingering bridge(s) removed - manually. +> **NOTE:** Running a session with a control network can fail if a previous +session has set up a control network and the its bridge is still up. Close +the previous session first or wait for it to complete. If unable to, the +*core-daemon* may need to be restarted and the lingering bridge(s) removed +manually. ```shell # Restart the CORE Daemon @@ -53,13 +52,11 @@ for cb in $ctrlbridges; do done ``` -!!! note - - If adjustments to the primary control network configuration made in - **/etc/core/core.conf** do not seem to take affect, check if there is anything - set in the *Session Menu*, the *Options...* dialog. They may need to be - cleared. These per session settings override the defaults in - **/etc/core/core.conf**. +> **NOTE:** If adjustments to the primary control network configuration made in +*/etc/core/core.conf* do not seem to take affect, check if there is anything +set in the *Session Menu*, the *Options...* dialog. They may need to be +cleared. These per session settings override the defaults in +*/etc/core/core.conf*. ## Control Network in Distributed Sessions @@ -105,9 +102,9 @@ argument being the keyword *"shutdown"*. Starting with EMANE 0.9.2, CORE will run EMANE instances within namespaces. Since it is advisable to separate the OTA traffic from other traffic, we will need more than single channel leading out from the namespace. Up to three -auxiliary control networks may be defined. Multiple control networks are set -up in */etc/core/core.conf* file. Lines *controlnet1*, *controlnet2* and -*controlnet3* define the auxiliary networks. +auxiliary control networks may be defined. Multiple control networks are set +up in */etc/core/core.conf* file. Lines *controlnet1*, *controlnet2* and +*controlnet3* define the auxiliary networks. For example, having the following */etc/core/core.conf*: @@ -117,20 +114,18 @@ controlnet1 = core1:172.18.1.0/24 core2:172.18.2.0/24 core3:172.18.3.0/24 controlnet2 = core1:172.19.1.0/24 core2:172.19.2.0/24 core3:172.19.3.0/24 ``` -This will activate the primary and two auxiliary control networks and add +This will activate the primary and two auxiliary control networks and add interfaces *ctrl0*, *ctrl1*, *ctrl2* to each node. One use case would be to assign *ctrl1* to the OTA manager device and *ctrl2* to the Event Service device in the EMANE Options dialog box and leave *ctrl0* for CORE control traffic. -!!! note - - *controlnet0* may be used in place of *controlnet* to configure - the primary control network. +> **NOTE:** *controlnet0* may be used in place of *controlnet* to configure +>the primary control network. Unlike the primary control network, the auxiliary control networks will not -employ tunneling since their primary purpose is for efficiently transporting -multicast EMANE OTA and event traffic. Note that there is no per-session +employ tunneling since their primary purpose is for efficiently transporting +multicast EMANE OTA and event traffic. Note that there is no per-session configuration for auxiliary control networks. To extend the auxiliary control networks across a distributed test @@ -144,11 +139,9 @@ controlnetif2 = eth2 controlnetif3 = eth3 ``` -!!! note - - There is no need to assign an interface to the primary control - network because tunnels are formed between the master and the slaves using IP - addresses that are provided in *servers.conf*. +> **NOTE:** There is no need to assign an interface to the primary control +>network because tunnels are formed between the master and the slaves using IP +>addresses that are provided in *servers.conf*. Shown below is a representative diagram of the configuration above. diff --git a/docs/devguide.md b/docs/devguide.md index 4fa43977..fe25e306 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -1,6 +1,9 @@ # CORE Developer's Guide -## Overview +* Table of Contents +{:toc} + +## Repository Overview The CORE source consists of several programming languages for historical reasons. Current development focuses on the Python modules and @@ -51,6 +54,9 @@ conveniently run tests, etc. # run core-daemon sudo core-daemon +# run python gui +core-pygui + # run gui core-gui @@ -62,7 +68,7 @@ inv test-mock ## Linux Network Namespace Commands Linux network namespace containers are often managed using the *Linux Container Tools* or *lxc-tools* package. -The lxc-tools website is available here http://lxc.sourceforge.net/ for more information. CORE does not use these +The lxc-tools website is available here http://lxc.sourceforge.net/ for more information. CORE does not use these management utilities, but includes its own set of tools for instantiating and configuring network namespace containers. This section describes these tools. @@ -97,7 +103,7 @@ vcmd -c /tmp/pycore.50160/n1 -- /sbin/ip -4 ro A script named *core-cleanup* is provided to clean up any running CORE emulations. It will attempt to kill any remaining vnoded processes, kill any EMANE processes, remove the :file:`/tmp/pycore.*` session directories, and remove -any bridges or *nftables* rules. With a *-d* option, it will also kill any running CORE daemon. +any bridges or *nftables* rules. With a *-d* option, it will also kill any running CORE daemon. ### netns command diff --git a/docs/distributed.md b/docs/distributed.md index 95ec7268..65429f03 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -1,5 +1,8 @@ # CORE - Distributed Emulation +* Table of Contents +{:toc} + ## Overview A large emulation scenario can be deployed on multiple emulation servers and @@ -58,7 +61,6 @@ First the distributed servers must be configured to allow passwordless root login over SSH. On distributed server: - ```shelll # install openssh-server sudo apt install openssh-server @@ -79,7 +81,6 @@ sudo systemctl restart sshd ``` On master server: - ```shell # install package if needed sudo apt install openssh-client @@ -98,7 +99,6 @@ connect_kwargs: {"key_filename": "/home/user/.ssh/core"} ``` On distributed server: - ```shell # open sshd config vi /etc/ssh/sshd_config @@ -116,9 +116,8 @@ Make sure the value used below is the absolute path to the file generated above **~/.ssh/core**" Add/update the fabric configuration file **/etc/fabric.yml**: - ```yaml -connect_kwargs: { "key_filename": "/home/user/.ssh/core" } +connect_kwargs: {"key_filename": "/home/user/.ssh/core"} ``` ## Add Emulation Servers in GUI @@ -170,10 +169,8 @@ only if an EMANE model is used for the WLAN. The basic range model does not work across multiple servers due to the Linux bridging and nftables rules that are used. -!!! note - - The basic range wireless model does not support distributed emulation, - but EMANE does. +**NOTE: The basic range wireless model does not support distributed emulation, +but EMANE does.** When nodes are linked across servers **core-daemons** will automatically create necessary tunnels between the nodes when executed. Care should be taken @@ -184,10 +181,10 @@ These tunnels are created using GRE tunneling, similar to the Tunnel Tool. ## Distributed Checklist 1. Install CORE on master server -2. Install distributed CORE package on all servers needed -3. Installed and configure public-key SSH access on all servers (if you want to use - double-click shells or Widgets.) for both the GUI user (for terminals) and root for running CORE commands -4. Update CORE configuration as needed -5. Choose the servers that participate in distributed emulation. -6. Assign nodes to desired servers, empty for master server. -7. Press the **Start** button to launch the distributed emulation. +1. Install distributed CORE package on all servers needed +1. Installed and configure public-key SSH access on all servers (if you want to use +double-click shells or Widgets.) for both the GUI user (for terminals) and root for running CORE commands +1. Update CORE configuration as needed +1. Choose the servers that participate in distributed emulation. +1. Assign nodes to desired servers, empty for master server. +1. Press the **Start** button to launch the distributed emulation. diff --git a/docs/docker.md b/docs/docker.md index 562fd453..0c730369 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -15,6 +15,7 @@ sudo apt install docker.io ### RHEL Systems + ## Configuration Custom configuration required to avoid iptable rules being added and removing @@ -25,8 +26,8 @@ Place the file below in **/etc/docker/docker.json** ```json { - "bridge": "none", - "iptables": false + "bridge": "none", + "iptables": false } ``` @@ -52,7 +53,6 @@ Images used by Docker nodes in CORE need to have networking tools installed for CORE to automate setup and configuration of the network within the container. Example Dockerfile: - ``` FROM ubuntu:latest RUN apt-get update @@ -60,7 +60,6 @@ RUN apt-get install -y iproute2 ethtool ``` Build image: - ```shell sudo docker build -t . ``` diff --git a/docs/emane.md b/docs/emane.md index a034c63b..dfa30897 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -1,4 +1,7 @@ -# EMANE (Extendable Mobile Ad-hoc Network Emulator) +# CORE/EMANE + +* Table of Contents +{:toc} ## What is EMANE? @@ -28,7 +31,7 @@ and instantiates one EMANE process in the namespace. The EMANE process binds a user space socket to the TAP device for sending and receiving data from CORE. An EMANE instance sends and receives OTA (Over-The-Air) traffic to and from -other EMANE instances via a control port (e.g. *ctrl0*, *ctrl1*). It also +other EMANE instances via a control port (e.g. *ctrl0*, *ctrl1*). It also sends and receives Events to and from the Event Service using the same or a different control port. EMANE models are configured through the GUI's configuration dialog. A corresponding EmaneModel Python class is sub-classed @@ -57,9 +60,7 @@ You can find more detailed tutorials and examples at the Every topic below assumes CORE, EMANE, and OSPF MDR have been installed. -!!! info - - Demo files will be found within the `core-gui` **~/.coregui/xmls** directory +> **WARNING:** demo files will be found within the new `core-pygui` | Topic | Model | Description | |--------------------------------------|---------|-----------------------------------------------------------| @@ -91,10 +92,8 @@ If you have an EMANE event generator (e.g. mobility or pathloss scripts) and want to have CORE subscribe to EMANE location events, set the following line in the **core.conf** configuration file. -!!! note - - Do not set this option to True if you want to manually drag nodes around - on the canvas to update their location in EMANE. +> **NOTE:** Do not set this option to True if you want to manually drag nodes around +on the canvas to update their location in EMANE. ```shell emane_event_monitor = True @@ -105,7 +104,6 @@ prefix will place the DTD files in **/usr/local/share/emane/dtd** while CORE expects them in **/usr/share/emane/dtd**. Update the EMANE prefix configuration to resolve this problem. - ```shell emane_prefix = /usr/local ``` @@ -118,7 +116,6 @@ placed within the path defined by **emane_models_dir** in the CORE configuration file. This path cannot end in **/emane**. Here is an example model with documentation describing functionality: - ```python """ Example custom emane model. @@ -184,7 +181,6 @@ class ExampleModel(emanemodel.EmaneModel): :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 @@ -214,7 +210,7 @@ The EMANE models should be listed here for selection. (You may need to restart t CORE daemon if it was running prior to installing the EMANE Python bindings.) When an EMANE model is selected, you can click on the models option button -causing the GUI to query the CORE daemon for configuration items. +causing the GUI to query the CORE daemon for configuration items. Each model will have different parameters, refer to the EMANE documentation for an explanation of each item. The defaults values are presented in the dialog. Clicking *Apply* and *Apply* again will store the @@ -224,7 +220,7 @@ The RF-PIPE and IEEE 802.11abg models use a Universal PHY that supports geographic location information for determining pathloss between nodes. A default latitude and longitude location is provided by CORE and this location-based pathloss is enabled by default; this is the *pathloss mode* -setting for the Universal PHY. Moving a node on the canvas while the +setting for the Universal PHY. Moving a node on the canvas while the emulation is running generates location events for EMANE. To view or change the geographic location or scale of the canvas use the *Canvas Size and Scale* dialog available from the *Canvas* menu. @@ -241,7 +237,7 @@ to be created in the virtual nodes that are linked to the EMANE WLAN. These devices appear with interface names such as eth0, eth1, etc. The EMANE processes should now be running in each namespace. -To view the configuration generated by CORE, look in the */tmp/pycore.nnnnn/* session +To view the configuration generated by CORE, look in the */tmp/pycore.nnnnn/* session directory to find the generated EMANE xml files. One easy way to view this information is by double-clicking one of the virtual nodes and listing the files in the shell. @@ -283,15 +279,14 @@ being used, along with changing any configuration setting from their defaults. ![](static/emane-configuration.png) -!!! note +> **NOTE:** Here is a quick checklist for distributed emulation with EMANE. - Here is a quick checklist for distributed emulation with EMANE. + 1. Follow the steps outlined for normal CORE. + 2. Assign nodes to desired servers + 3. Synchronize your machine's clocks prior to starting the emulation, + using *ntp* or *ptp*. Some EMANE models are sensitive to timing. + 4. Press the *Start* button to launch the distributed emulation. -1. Follow the steps outlined for normal CORE. -2. Assign nodes to desired servers -3. Synchronize your machine's clocks prior to starting the emulation, - using *ntp* or *ptp*. Some EMANE models are sensitive to timing. -4. Press the *Start* button to launch the distributed emulation. Now when the Start button is used to instantiate the emulation, the local CORE daemon will connect to other emulation servers that have been assigned diff --git a/docs/emane/antenna.md b/docs/emane/antenna.md index 79c023ac..c8a86eaa 100644 --- a/docs/emane/antenna.md +++ b/docs/emane/antenna.md @@ -1,7 +1,8 @@ # EMANE Antenna Profiles +* Table of Contents +{:toc} ## Overview - Introduction to using the EMANE antenna profile in CORE, based on the example EMANE Demo linked below. @@ -9,348 +10,340 @@ EMANE Demo linked below. for more specifics. ## Demo Setup - We will need to create some files in advance of starting this session. Create directory to place antenna profile files. - ```shell mkdir /tmp/emane ``` Create `/tmp/emane/antennaprofile.xml` with the following contents. - ```xml - - - - - - + + + + + + ``` Create `/tmp/emane/antenna30dsector.xml` with the following contents. - ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ``` Create `/tmp/emane/blockageaft.xml` with the following contents. - ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ``` ## Run Demo - 1. Select `Open...` within the GUI 1. Load `emane-demo-antenna.xml` 1. Click ![Start Button](../static/gui/start.png) 1. After startup completes, double click n1 to bring up the nodes terminal ## Example Demo - This demo will cover running an EMANE event service to feed in antenna, location, and pathloss events to demonstrate how antenna profiles can be used. ### EMANE Event Dump - On n1 lets dump EMANE events, so when we later run the EMANE event service you can monitor when and what is sent. @@ -359,44 +352,38 @@ root@n1:/tmp/pycore.44917/n1.conf# emaneevent-dump -i ctrl0 ``` ### Send EMANE Events - On the host machine create the following to send EMANE events. -!!! warning - - Make sure to set the `eventservicedevice` to the proper control - network value +> **WARNING:** make sure to set the `eventservicedevice` to the proper control +> network value Create `eventservice.xml` with the following contents. - ```xml - - - + + + ``` Create `eelgenerator.xml` with the following contents. - ```xml - + - - - - + + + + ``` Create `scenario.eel` with the following contents. - ```shell 0.0 nem:1 antennaprofile 1,0.0,0.0 0.0 nem:4 antennaprofile 2,0.0,0.0 @@ -426,25 +413,23 @@ Create `scenario.eel` with the following contents. Run the EMANE event service, monitor what is output on n1 for events dumped and see the link changes within the CORE GUI. - ```shell emaneeventservice -l 3 eventservice.xml ``` ### Stages - The events sent will trigger 4 different states. * State 1 - * n2 and n3 see each other - * n4 and n3 are pointing away + * n2 and n3 see each other + * n4 and n3 are pointing away * State 2 - * n2 and n3 see each other - * n1 and n2 see each other - * n4 and n3 see each other + * n2 and n3 see each other + * n1 and n2 see each other + * n4 and n3 see each other * State 3 - * n2 and n3 see each other - * n4 and n3 are pointing at each other but blocked + * n2 and n3 see each other + * n4 and n3 are pointing at each other but blocked * State 4 - * n2 and n3 see each other - * n4 and n3 see each other + * n2 and n3 see each other + * n4 and n3 see each other diff --git a/docs/emane/eel.md b/docs/emane/eel.md index c2dad86a..ca094542 100644 --- a/docs/emane/eel.md +++ b/docs/emane/eel.md @@ -1,51 +1,44 @@ # EMANE Emulation Event Log (EEL) Generator +* Table of Contents +{:toc} ## Overview - Introduction to using the EMANE event service and eel files to provide events. [EMANE Demo 1](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-1) for more specifics. ## Run Demo - 1. Select `Open...` within the GUI -2. Load `emane-demo-eel.xml` -3. Click ![Start Button](../static/gui/start.png) -4. After startup completes, double click n1 to bring up the nodes terminal +1. Load `emane-demo-eel.xml` +1. Click ![Start Button](../static/gui/start.png) +1. After startup completes, double click n1 to bring up the nodes terminal ## Example Demo - This demo will go over defining an EMANE event service and eel file to drive an emane event service. ### Viewing Events - On n1 we will use the EMANE event dump utility to listen to events. - ```shell root@n1:/tmp/pycore.46777/n1.conf# emaneevent-dump -i ctrl0 ``` ### Sending Events - On the host machine we will create the following files and start the EMANE event service targeting the control network. -!!! warning - - Make sure to set the `eventservicedevice` to the proper control - network value +> **WARNING:** make sure to set the `eventservicedevice` to the proper control +> network value Create `eventservice.xml` with the following contents. - ```xml - - - + + + ``` @@ -64,23 +57,21 @@ These configuration items tell the EEL Generator which sentences to map to which plugin and whether to issue delta or full updates. Create `eelgenerator.xml` with the following contents. - ```xml - + - - - - + + + + ``` Finally, create `scenario.eel` with the following contents. - ```shell 0.0 nem:1 pathloss nem:2,90.0 0.0 nem:2 pathloss nem:1,90.0 @@ -89,13 +80,11 @@ Finally, create `scenario.eel` with the following contents. ``` Start the EMANE event service using the files created above. - ```shell emaneeventservice eventservice.xml -l 3 ``` ### Sent Events - If we go back to look at our original terminal we will see the events logged out to the terminal. diff --git a/docs/emane/files.md b/docs/emane/files.md index c04b0f6b..c9bc35e8 100644 --- a/docs/emane/files.md +++ b/docs/emane/files.md @@ -1,7 +1,8 @@ # EMANE XML Files +* Table of Contents +{:toc} ## Overview - Introduction to the XML files generated by CORE used to drive EMANE for a given node. @@ -9,14 +10,12 @@ a given node. may provide more helpful details. ## Run Demo - 1. Select `Open...` within the GUI -2. Load `emane-demo-files.xml` -3. Click ![Start Button](../static/gui/start.png) -4. After startup completes, double click n1 to bring up the nodes terminal +1. Load `emane-demo-files.xml` +1. Click ![Start Button](../static/gui/start.png) +1. After startup completes, double click n1 to bring up the nodes terminal ## Example Demo - We will take a look at the files generated in the example demo provided. In this case we are running the RF Pipe model. @@ -32,7 +31,6 @@ case we are running the RF Pipe model. | \-trans.xml | configuration when a raw transport is being used | ### Listing File - Below are the files within n1 after starting the demo session. ```shell @@ -43,7 +41,6 @@ eth0-phy.xml n1-emane.log usr.local.etc.quagga var.run.quagga ``` ### Platform XML - The root configuration file used to run EMANE for a node is the platform xml file. In this demo we are looking at `n1-platform.xml`. @@ -81,7 +78,6 @@ root@n1:/tmp/pycore.46777/n1.conf# cat n1-platform.xml ``` ### NEM XML - The nem definition will contain reference to the transport, mac, and phy xml definitions being used for a given nem. @@ -97,7 +93,6 @@ root@n1:/tmp/pycore.46777/n1.conf# cat eth0-nem.xml ``` ### MAC XML - MAC layer configuration settings would be found in this file. CORE will write out all values, even if the value is a default value. @@ -120,7 +115,6 @@ root@n1:/tmp/pycore.46777/n1.conf# cat eth0-mac.xml ``` ### PHY XML - PHY layer configuration settings would be found in this file. CORE will write out all values, even if the value is a default value. @@ -155,7 +149,6 @@ root@n1:/tmp/pycore.46777/n1.conf# cat eth0-phy.xml ``` ### Transport XML - ```shell root@n1:/tmp/pycore.46777/n1.conf# cat eth0-trans-virtual.xml diff --git a/docs/emane/gpsd.md b/docs/emane/gpsd.md index eadf8af2..f20cc8fe 100644 --- a/docs/emane/gpsd.md +++ b/docs/emane/gpsd.md @@ -1,62 +1,54 @@ # EMANE GPSD Integration +* Table of Contents +{:toc} ## Overview - Introduction to integrating gpsd in CORE with EMANE. [EMANE Demo 0](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-0) may provide more helpful details. -!!! warning - - Requires installation of [gpsd](https://gpsd.gitlab.io/gpsd/index.html) +> **WARNING:** requires installation of [gpsd](https://gpsd.gitlab.io/gpsd/index.html) ## Run Demo - 1. Select `Open...` within the GUI -2. Load `emane-demo-gpsd.xml` -3. Click ![Start Button](../static/gui/start.png) -4. After startup completes, double click n1 to bring up the nodes terminal +1. Load `emane-demo-gpsd.xml` +1. Click ![Start Button](../static/gui/start.png) +1. After startup completes, double click n1 to bring up the nodes terminal ## Example Demo - This section will cover how to run a gpsd location agent within EMANE, that will write out locations to a pseudo terminal file. That file can be read in by the gpsd server and make EMANE location events available to gpsd clients. ### EMANE GPSD Event Daemon - First create an `eventdaemon.xml` file on n1 with the following contents. - ```xml - - - + + + ``` Then create the `gpsdlocationagent.xml` file on n1 with the following contents. - ```xml - + ``` Start the EMANE event agent. This will facilitate feeding location events out to a pseudo terminal file defined above. - ```shell emaneeventd eventdaemon.xml -r -d -l 3 -f emaneeventd.log ``` Start gpsd, reading in the pseudo terminal file. - ```shell gpsd -G -n -b $(cat gps.pty) ``` @@ -67,41 +59,36 @@ EEL Events will be played out from the actual host machine over the designated control network interface. Create the following files in the same directory somewhere on your host. -!!! note - - Make sure the below eventservicedevice matches the control network - device being used on the host for EMANE +> **NOTE:** make sure the below eventservicedevice matches the control network +> device being used on the host for EMANE Create `eventservice.xml` on the host machine with the following contents. - ```xml - - - + + + ``` Create `eelgenerator.xml` on the host machine with the following contents. - ```xml - + - - - - + + + + ``` Create `scenario.eel` file with the following contents. - ```shell 0.0 nem:1 location gps 40.031075,-74.523518,3.000000 0.0 nem:2 location gps 40.031165,-74.523412,3.000000 @@ -109,8 +96,7 @@ Create `scenario.eel` file with the following contents. Start the EEL event service, which will send the events defined in the file above over the control network to all EMANE nodes. These location events will be received -and provided to gpsd. This allows gpsd client to connect to and get gps locations. - +and provided to gpsd. This allow gpsd client to connect to and get gps locations. ```shell emaneeventservice eventservice.xml -l 3 ``` diff --git a/docs/emane/precomputed.md b/docs/emane/precomputed.md index 4d0234ae..53da75eb 100644 --- a/docs/emane/precomputed.md +++ b/docs/emane/precomputed.md @@ -1,40 +1,35 @@ # EMANE Procomputed +* Table of Contents +{:toc} ## Overview - Introduction to using the precomputed propagation model. [EMANE Demo 1](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-1) for more specifics. ## Run Demo - 1. Select `Open...` within the GUI -2. Load `emane-demo-precomputed.xml` -3. Click ![Start Button](../static/gui/start.png) -4. After startup completes, double click n1 to bring up the nodes terminal +1. Load `emane-demo-precomputed.xml` +1. Click ![Start Button](../static/gui/start.png) +1. After startup completes, double click n1 to bring up the nodes terminal ## Example Demo - -This demo is using the RF Pipe model with the propagation model set to +This demo is uing the RF Pipe model witht he propagation model set to precomputed. ### Failed Pings - Due to using precomputed and having not sent any pathloss events, the nodes -cannot ping each other yet. +cannot ping eachother yet. Open a terminal on n1. - ```shell root@n1:/tmp/pycore.46777/n1.conf# ping 10.0.0.2 connect: Network is unreachable ``` ### EMANE Shell - You can leverage `emanesh` to investigate why packets are being dropped. - ```shell root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy BroadcastPacketDropTable0 UnicastPacketDropTable0 nem 1 phy BroadcastPacketDropTable0 @@ -48,7 +43,6 @@ nem 1 phy UnicastPacketDropTable0 In the example above we can see that the reason packets are being dropped is due to the propogation model and that is because we have not issued any pathloss events. You can run another command to validate if you have received any pathloss events. - ```shell root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy PathlossEventInfoTable nem 1 phy PathlossEventInfoTable @@ -56,19 +50,15 @@ nem 1 phy PathlossEventInfoTable ``` ### Pathloss Events - On the host we will send pathloss events from all nems to all other nems. -!!! note - - Make sure properly specify the right control network device +> **NOTE:** make sure properly specify the right control network device ```shell emaneevent-pathloss 1:2 90 -i ``` Now if we check for pathloss events on n2 we will see what was just sent above. - ```shell root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy PathlossEventInfoTable nem 1 phy PathlossEventInfoTable @@ -77,7 +67,6 @@ nem 1 phy PathlossEventInfoTable ``` You should also now be able to ping n1 from n2. - ```shell root@n1:/tmp/pycore.46777/n1.conf# ping -c 3 10.0.0.2 PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. diff --git a/docs/grpc.md b/docs/grpc.md index 3266a57d..aef79308 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -1,6 +1,7 @@ -* Table of Contents +# gRPC API -## Overview +* Table of Contents +{:toc} [gRPC](https://grpc.io/) is a client/server API for interfacing with CORE and used by the python GUI for driving all functionality. It is dependent @@ -8,7 +9,7 @@ on having a running `core-daemon` instance to be leveraged. A python client can be created from the raw generated grpc files included with CORE or one can leverage a provided gRPC client that helps encapsulate -some functionality to try and help make things easier. +some of the functionality to try and help make things easier. ## Python Client @@ -18,7 +19,7 @@ to help provide some conveniences when using the API. ### Client HTTP Proxy -Since gRPC is HTTP2 based, proxy configurations can cause issues. By default, +Since gRPC is HTTP2 based, proxy configurations can cause issues. By default the client disables proxy support to avoid issues when a proxy is present. You can enable and properly account for this issue when needed. @@ -40,13 +41,13 @@ When creating nodes of type `NodeType.DEFAULT` these are the default models and the services they map to. * mdr - * zebra, OSPFv3MDR, IPForward + * zebra, OSPFv3MDR, IPForward * PC - * DefaultRoute + * DefaultRoute * router - * zebra, OSPFv2, OSPFv3, IPForward + * zebra, OSPFv2, OSPFv3, IPForward * host - * DefaultRoute, SSH + * DefaultRoute, SSH ### Interface Helper @@ -55,10 +56,8 @@ when creating interface data for nodes. Alternatively one can manually create a `core.api.grpc.wrappers.Interface` class instead with appropriate information. Manually creating gRPC client interface: - ```python from core.api.grpc.wrappers import Interface - # id is optional and will set to the next available id # name is optional and will default to eth # mac is optional and will result in a randomly generated mac @@ -73,7 +72,6 @@ iface = Interface( ``` Leveraging the interface helper class: - ```python from core.api.grpc import client @@ -92,7 +90,6 @@ iface_data = iface_helper.create_iface( Various events that can occur within a session can be listened to. Event types: - * session - events for changes in session state and mobility start/stop/pause * node - events for node movements and icon changes * link - events for link configuration changes and wireless link add/delete @@ -104,11 +101,9 @@ Event types: from core.api.grpc import client from core.api.grpc.wrappers import EventType - def event_listener(event): print(event) - # create grpc client and connect core = client.CoreGrpcClient() core.connect() @@ -128,7 +123,6 @@ core.events(session.id, event_listener, [EventType.NODE]) Links can be configured at the time of creation or during runtime. Currently supported configuration options: - * bandwidth (bps) * delay (us) * duplicate (%) @@ -173,11 +167,10 @@ core.edit_link(session.id, link) ``` ### Peer to Peer Example - ```python # required imports from core.api.grpc import client -from core.api.grpc.wrappers import Position +from core.api.grpc.core_pb2 import Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -205,11 +198,10 @@ core.start_session(session) ``` ### Switch/Hub Example - ```python # required imports from core.api.grpc import client -from core.api.grpc.wrappers import NodeType, Position +from core.api.grpc.core_pb2 import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -240,11 +232,10 @@ core.start_session(session) ``` ### WLAN Example - ```python # required imports from core.api.grpc import client -from core.api.grpc.wrappers import NodeType, Position +from core.api.grpc.core_pb2 import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -292,7 +283,6 @@ For EMANE you can import and use one of the existing models and use its name for configuration. Current models: - * core.emane.ieee80211abg.EmaneIeee80211abgModel * core.emane.rfpipe.EmaneRfPipeModel * core.emane.tdma.EmaneTdmaModel @@ -309,7 +299,7 @@ will use the defaults. When no configuration is used, the defaults are used. ```python # required imports from core.api.grpc import client -from core.api.grpc.wrappers import NodeType, Position +from core.api.grpc.core_pb2 import NodeType, Position from core.emane.models.ieee80211abg import EmaneIeee80211abgModel # interface helper @@ -325,7 +315,7 @@ session = core.create_session() # create nodes position = Position(x=200, y=200) emane = session.add_node( - 1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name + 1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name ) position = Position(x=100, y=100) node1 = session.add_node(2, model="mdr", position=position) @@ -340,8 +330,8 @@ session.add_link(node1=node2, node2=emane, iface1=iface1) # setting emane specific emane model configuration emane.set_emane_model(EmaneIeee80211abgModel.name, { - "eventservicettl": "2", - "unicastrate": "3", + "eventservicettl": "2", + "unicastrate": "3", }) # start session @@ -349,7 +339,6 @@ core.start_session(session) ``` EMANE Model Configuration: - ```python # emane network specific config, set on an emane node # this setting applies to all nodes connected @@ -370,7 +359,6 @@ Configuring the files of a service results in a specific hard coded script being generated, instead of the default scripts, that may leverage dynamic generation. The following features can be configured for a service: - * files - files that will be generated * directories - directories that will be mounted unique to the node * startup - commands to run start a service @@ -378,7 +366,6 @@ The following features can be configured for a service: * shutdown - commands to run to stop a service Editing service properties: - ```python # configure a service, for a node, for a given session node.service_configs[service_name] = NodeServiceData( @@ -394,7 +381,6 @@ When editing a service file, it must be the name of `config` file that the service will generate. Editing a service file: - ```python # to edit the contents of a generated file you can specify # the service, the file name, and its contents @@ -405,7 +391,7 @@ file_configs[file_name] = "echo hello world" ## File Examples File versions of the network examples can be found -[here](https://github.com/coreemu/core/tree/master/package/examples/grpc). +[here](https://github.com/coreemu/core/tree/master/daemon/examples/grpc). These examples will create a session using the gRPC API when the core-daemon is running. You can then switch to and attach to these sessions using either of the CORE GUIs. diff --git a/docs/gui.md b/docs/gui.md index c296ac18..633cf829 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -1,5 +1,9 @@ + # CORE GUI +* Table of Contents +{:toc} + ![](static/core-gui.png) ## Overview @@ -8,7 +12,7 @@ The GUI is used to draw nodes and network devices on a canvas, linking them together to create an emulated network session. After pressing the start button, CORE will proceed through these phases, -staying in the **runtime** phase. After the session is stopped, CORE will +staying in the **runtime** phase. After the session is stopped, CORE will proceed to the **data collection** phase before tearing down the emulated state. @@ -18,7 +22,7 @@ when these session states are reached. ## Prerequisites -Beyond installing CORE, you must have the CORE daemon running. This is done +Beyond installing CORE, you must have the CORE daemon running. This is done on the command line with either systemd or sysv. ```shell @@ -36,29 +40,29 @@ The GUI will create a directory in your home directory on first run called ~/.coregui. This directory will help layout various files that the GUI may use. * .coregui/ - * backgrounds/ - * place backgrounds used for display in the GUI - * custom_emane/ - * place to keep custom emane models to use with the core-daemon - * custom_services/ - * place to keep custom services to use with the core-daemon - * icons/ - * icons the GUI uses along with customs icons desired - * mobility/ - * place to keep custom mobility files - * scripts/ - * place to keep core related scripts - * xmls/ - * place to keep saved session xml files - * gui.log - * log file when running the gui, look here when issues occur for exceptions etc - * config.yaml - * configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc) + * backgrounds/ + * place backgrounds used for display in the GUI + * custom_emane/ + * place to keep custom emane models to use with the core-daemon + * custom_services/ + * place to keep custom services to use with the core-daemon + * icons/ + * icons the GUI uses along with customs icons desired + * mobility/ + * place to keep custom mobility files + * scripts/ + * place to keep core related scripts + * xmls/ + * place to keep saved session xml files + * gui.log + * log file when running the gui, look here when issues occur for exceptions etc + * config.yaml + * configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc) ## Modes of Operation The CORE GUI has two primary modes of operation, **Edit** and **Execute** -modes. Running the GUI, by typing **core-gui** with no options, starts in +modes. Running the GUI, by typing **core-pygui** with no options, starts in Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left and configured from right-click menus or by double-clicking them. The GUI does not need to be run as root. @@ -305,6 +309,168 @@ and options. | CORE Documentation (www) | Lnk to the CORE Documentation page. | | About | Invokes the About dialog box for viewing version information. | +## Connecting with Physical Networks + +CORE's emulated networks run in real time, so they can be connected to live +physical networks. The RJ45 tool and the Tunnel tool help with connecting to +the real world. These tools are available from the **Link-layer nodes** menu. + +When connecting two or more CORE emulations together, MAC address collisions +should be avoided. CORE automatically assigns MAC addresses to interfaces when +the emulation is started, starting with **00:00:00:aa:00:00** and incrementing +the bottom byte. The starting byte should be changed on the second CORE machine +using the **Tools->MAC Addresses** option the menu. + +### RJ45 Tool + +The RJ45 node in CORE represents a physical interface on the real CORE machine. +Any real-world network device can be connected to the interface and communicate +with the CORE nodes in real time. + +The main drawback is that one physical interface is required for each +connection. When the physical interface is assigned to CORE, it may not be used +for anything else. Another consideration is that the computer or network that +you are connecting to must be co-located with the CORE machine. + +To place an RJ45 connection, click on the **Link-layer nodes** toolbar and select +the **RJ45 Tool** from the submenu. Click on the canvas near the node you want to +connect to. This could be a router, hub, switch, or WLAN, for example. Now +click on the *Link Tool* and draw a link between the RJ45 and the other node. +The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a +physical interface. A list of available interfaces will be shown, and one may +be selected by double-clicking its name in the list, or an interface name may +be entered into the text box. + +> **NOTE:** When you press the Start button to instantiate your topology, the + interface assigned to the RJ45 will be connected to the CORE topology. The + interface can no longer be used by the system. + +Multiple RJ45 nodes can be used within CORE and assigned to the same physical +interface if 802.1x VLANs are used. This allows for more RJ45 nodes than +physical ports are available, but the (e.g. switching) hardware connected to +the physical port must support the VLAN tagging, and the available bandwidth +will be shared. + +You need to create separate VLAN virtual devices on the Linux host, +and then assign these devices to RJ45 nodes inside of CORE. The VLANning is +actually performed outside of CORE, so when the CORE emulated node receives a +packet, the VLAN tag will already be removed. + +Here are example commands for creating VLAN devices under Linux: + +```shell +ip link add link eth0 name eth0.1 type vlan id 1 +ip link add link eth0 name eth0.2 type vlan id 2 +ip link add link eth0 name eth0.3 type vlan id 3 +``` + +### Tunnel Tool + +The tunnel tool builds GRE tunnels between CORE emulations or other hosts. +Tunneling can be helpful when the number of physical interfaces is limited or +when the peer is located on a different network. Also a physical interface does +not need to be dedicated to CORE as with the RJ45 tool. + +The peer GRE tunnel endpoint may be another CORE machine or another +host that supports GRE tunneling. When placing a Tunnel node, initially +the node will display "UNASSIGNED". This text should be replaced with the IP +address of the tunnel peer. This is the IP address of the other CORE machine or +physical machine, not an IP address of another virtual node. + +> **NOTE:** Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device + has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the + bridge's MTU + becomes 1,458 bytes. The Linux bridge will not perform fragmentation for + large packets if other bridge ports have a higher MTU such as 1,500 bytes. + +The GRE key is used to identify flows with GRE tunneling. This allows multiple +GRE tunnels to exist between that same pair of tunnel peers. A unique number +should be used when multiple tunnels are used with the same peer. When +configuring the peer side of the tunnel, ensure that the matching keys are +used. + +Here are example commands for building the other end of a tunnel on a Linux +machine. In this example, a router in CORE has the virtual address +**10.0.0.1/24** and the CORE host machine has the (real) address +**198.51.100.34/24**. The Linux box +that will connect with the CORE machine is reachable over the (real) network +at **198.51.100.76/24**. +The emulated router is linked with the Tunnel Node. In the +Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with +the key set to **1**. The gretap interface on the Linux box will be assigned +an address from the subnet of the virtual router node, +**10.0.0.2/24**. + +```shell +# these commands are run on the tunnel peer +sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1 +sudo ip addr add 10.0.0.2/24 dev gt0 +sudo ip link set dev gt0 up +``` + +Now the virtual router should be able to ping the Linux machine: + +```shell +# from the CORE router node +ping 10.0.0.2 +``` + +And the Linux machine should be able to ping inside the CORE emulation: + +```shell +# from the tunnel peer +ping 10.0.0.1 +``` + +To debug this configuration, **tcpdump** can be run on the gretap devices, or +on the physical interfaces on the CORE or Linux machines. Make sure that a +firewall is not blocking the GRE traffic. + +### Communicating with the Host Machine + +The host machine that runs the CORE GUI and/or daemon is not necessarily +accessible from a node. Running an X11 application on a node, for example, +requires some channel of communication for the application to connect with +the X server for graphical display. There are different ways to +connect from the node to the host and vice versa. + +#### Control Network + +The quickest way to connect with the host machine through the primary control +network. + +With a control network, the host can launch an X11 application on a node. +To run an X11 application on the node, the **SSH** service can be enabled on +the node, and SSH with X11 forwarding can be used from the host to the node. + +```shell +# SSH from host to node n5 to run an X11 app +ssh -X 172.16.0.5 xclock +``` + +#### Other Methods + +There are still other ways to connect a host with a node. The RJ45 Tool +can be used in conjunction with a dummy interface to access a node: + +```shell +sudo modprobe dummy numdummies=1 +``` + +A **dummy0** interface should appear on the host. Use the RJ45 tool assigned +to **dummy0**, and link this to a node in your scenario. After starting the +session, configure an address on the host. + +```shell +sudo ip link show type bridge +# determine bridge name from the above command +# assign an IP address on the same network as the linked node +sudo ip addr add 10.0.1.2/24 dev b.48304.34658 +``` + +In the example shown above, the host will have the address **10.0.1.2** and +the node linked to the RJ45 may have the address **10.0.1.1**. + ## Building Sample Networks ### Wired Networks @@ -329,34 +495,23 @@ The wireless LAN (WLAN) is covered in the next section. ### Wireless Networks -Wireless networks allow moving nodes around to impact the connectivity between them. Connections between a -pair of nodes is stronger when the nodes are closer while connection is weaker when the nodes are further away. -CORE offers several levels of wireless emulation fidelity, depending on modeling needs and available -hardware. +The wireless LAN node allows you to build wireless networks where moving nodes +around affects the connectivity between them. Connection between a pair of nodes is stronger +when the nodes are closer while connection is weaker when the nodes are further away. +The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers +several levels of wireless emulation fidelity, depending on your modeling needs. -* WLAN Node - * uses set bandwidth, delay, and loss - * links are enabled or disabled based on a set range - * uses the least CPU when moving, but nothing extra when not moving -* Wireless Node - * uses set bandwidth, delay, and initial loss - * loss dynamically changes based on distance between nodes, which can be configured with range parameters - * links are enabled or disabled based on a set range - * uses more CPU to calculate loss for every movement, but nothing extra when not moving -* EMANE Node - * uses a physical layer model to account for signal propagation, antenna profile effects and interference - sources in order to provide a realistic environment for wireless experimentation - * uses the most CPU for every packet, as complex calculations are used for fidelity - * See [Wiki](https://github.com/adjacentlink/emane/wiki) for details on general EMANE usage - * See [CORE EMANE](emane.md) for details on using EMANE in CORE +The WLAN tool can be extended with plug-ins for different levels of wireless +fidelity. The basic on/off range is the default setting available on all +platforms. Other plug-ins offer higher fidelity at the expense of greater +complexity and CPU usage. The availability of certain plug-ins varies depending +on platform. See the table below for a brief overview of wireless model types. -| Model | Type | Supported Platform(s) | Fidelity | Description | -|----------|--------|-----------------------|----------|-------------------------------------------------------------------------------| -| WLAN | On/Off | Linux | Low | Ethernet bridging with nftables | -| Wireless | On/Off | Linux | Medium | Ethernet bridging with nftables | -| EMANE | RF | Linux | High | TAP device connected to EMANE emulator with pluggable MAC and PHY radio types | -#### Example WLAN Network Setup +| Model | Type | Supported Platform(s) | Fidelity | Description | +|-------|---------|-----------------------|----------|-------------------------------------------------------------------------------| +| Basic | on/off | Linux | Low | Ethernet bridging with nftables | +| EMANE | Plug-in | Linux | High | TAP device connected to EMANE emulator with pluggable MAC and PHY radio types | To quickly build a wireless network, you can first place several router nodes onto the canvas. If you have the @@ -378,26 +533,13 @@ The default configuration of the WLAN is set to use the basic range model. Havin selected causes **core-daemon** to calculate the distance between nodes based on screen pixels. A numeric range in screen pixels is set for the wireless network using the **Range** slider. When two wireless nodes are within range of -each other, a green line is drawn between them and they are linked. Two +each other, a green line is drawn between them and they are linked. Two wireless nodes that are farther than the range pixels apart are not linked. During Execute mode, users may move wireless nodes around by clicking and dragging them, and wireless links will be dynamically made or broken. -### Running Commands within Nodes - -You can double click a node to bring up a terminal for running shell commands. Within -the terminal you can run anything you like and those commands will be run in context of the node. -For standard CORE nodes, the only thing to keep in mind is that you are using the host file -system and anything you change or do can impact the greater system. By default, your terminal -will open within the nodes home directory for the running session, but it is temporary and -will be removed when the session is stopped. - -You can also launch GUI based applications from within standard CORE nodes, but you need to -enable xhost access to root. - -```shell -xhost +local:root -``` +The **EMANE Nodes** leverage available EMANE models to use for wireless networking. +See the [EMANE](emane.md) chapter for details on using EMANE. ### Mobility Scripting @@ -410,8 +552,7 @@ CORE has a few ways to script mobility. | EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. | For the first method, you can create a mobility script using a text -editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate -the script with one of the wireless +editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless using the WLAN configuration dialog box. Click the *ns-2 mobility script...* button, and set the *mobility script file* field in the resulting *ns2script* configuration dialog. diff --git a/docs/hitl.md b/docs/hitl.md deleted file mode 100644 index b659a36f..00000000 --- a/docs/hitl.md +++ /dev/null @@ -1,127 +0,0 @@ -# Hardware In The Loop - -## Overview - -In some cases it may be impossible or impractical to run software using CORE -nodes alone. You may need to bring in external hardware into the network. -CORE's emulated networks run in real time, so they can be connected to live -physical networks. The RJ45 tool and the Tunnel tool help with connecting to -the real world. These tools are available from the **Link Layer Nodes** menu. - -When connecting two or more CORE emulations together, MAC address collisions -should be avoided. CORE automatically assigns MAC addresses to interfaces when -the emulation is started, starting with **00:00:00:aa:00:00** and incrementing -the bottom byte. The starting byte should be changed on the second CORE machine -using the **Tools->MAC Addresses** option the menu. - -## RJ45 Node - -CORE provides the RJ45 node, which represents a physical -interface within the host that is running CORE. Any real-world network -devices can be connected to the interface and communicate with the CORE nodes in real time. - -The main drawback is that one physical interface is required for each -connection. When the physical interface is assigned to CORE, it may not be used -for anything else. Another consideration is that the computer or network that -you are connecting to must be co-located with the CORE machine. - -### GUI Usage - -To place an RJ45 connection, click on the **Link Layer Nodes** toolbar and select -the **RJ45 Node** from the options. Click on the canvas, where you would like -the nodes to place. Now click on the **Link Tool** and draw a link between the RJ45 -and the other node you wish to be connected to. The RJ45 node will display "UNASSIGNED". -Double-click the RJ45 node to assign a physical interface. A list of available -interfaces will be shown, and one may be selected, then selecting **Apply**. - -!!! note - - When you press the Start button to instantiate your topology, the - interface assigned to the RJ45 will be connected to the CORE topology. The - interface can no longer be used by the system. - -### Multiple RJ45s with One Interface (VLAN) - -It is possible to have multiple RJ45 nodes using the same physical interface -by leveraging 802.1x VLANs. This allows for more RJ45 nodes than physical ports -are available, but the (e.g. switching) hardware connected to the physical port -must support the VLAN tagging, and the available bandwidth will be shared. - -You need to create separate VLAN virtual devices on the Linux host, -and then assign these devices to RJ45 nodes inside of CORE. The VLANing is -actually performed outside of CORE, so when the CORE emulated node receives a -packet, the VLAN tag will already be removed. - -Here are example commands for creating VLAN devices under Linux: - -```shell -ip link add link eth0 name eth0.1 type vlan id 1 -ip link add link eth0 name eth0.2 type vlan id 2 -ip link add link eth0 name eth0.3 type vlan id 3 -``` - -## Tunnel Tool - -The tunnel tool builds GRE tunnels between CORE emulations or other hosts. -Tunneling can be helpful when the number of physical interfaces is limited or -when the peer is located on a different network. In this case a physical interface does -not need to be dedicated to CORE as with the RJ45 tool. - -The peer GRE tunnel endpoint may be another CORE machine or another -host that supports GRE tunneling. When placing a Tunnel node, initially -the node will display "UNASSIGNED". This text should be replaced with the IP -address of the tunnel peer. This is the IP address of the other CORE machine or -physical machine, not an IP address of another virtual node. - -!!! note - - Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. - The *gretap* device has an interface MTU of 1,458 bytes; when joined to a Linux - bridge, the bridge's MTU becomes 1,458 bytes. The Linux bridge will not perform - fragmentation for large packets if other bridge ports have a higher MTU such - as 1,500 bytes. - -The GRE key is used to identify flows with GRE tunneling. This allows multiple -GRE tunnels to exist between that same pair of tunnel peers. A unique number -should be used when multiple tunnels are used with the same peer. When -configuring the peer side of the tunnel, ensure that the matching keys are -used. - -### Example Usage - -Here are example commands for building the other end of a tunnel on a Linux -machine. In this example, a router in CORE has the virtual address -**10.0.0.1/24** and the CORE host machine has the (real) address -**198.51.100.34/24**. The Linux box -that will connect with the CORE machine is reachable over the (real) network -at **198.51.100.76/24**. -The emulated router is linked with the Tunnel Node. In the -Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with -the key set to **1**. The gretap interface on the Linux box will be assigned -an address from the subnet of the virtual router node, -**10.0.0.2/24**. - -```shell -# these commands are run on the tunnel peer -sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1 -sudo ip addr add 10.0.0.2/24 dev gt0 -sudo ip link set dev gt0 up -``` - -Now the virtual router should be able to ping the Linux machine: - -```shell -# from the CORE router node -ping 10.0.0.2 -``` - -And the Linux machine should be able to ping inside the CORE emulation: - -```shell -# from the tunnel peer -ping 10.0.0.1 -``` - -To debug this configuration, **tcpdump** can be run on the gretap devices, or -on the physical interfaces on the CORE or Linux machines. Make sure that a -firewall is not blocking the GRE traffic. diff --git a/docs/index.md b/docs/index.md index 4afec59f..e17a2e3b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,15 +4,32 @@ CORE (Common Open Research Emulator) is a tool for building virtual networks. As an emulator, CORE builds a representation of a real computer network that runs in real time, as opposed to simulation, where abstract models are -used. The live-running emulation can be connected to physical networks and routers. It provides an environment for +used. The live-running emulation can be connected to physical networks and routers. It provides an environment for running real applications and protocols, taking advantage of tools provided by the Linux operating system. CORE is typically used for network and protocol research, demonstrations, application and platform testing, evaluating networking scenarios, security studies, and increasing the size of physical test networks. ### Key Features - * Efficient and scalable * Runs applications and protocols without modification * Drag and drop GUI * Highly customizable + +## Topics + +| Topic | Description | +|--------------------------------------|-------------------------------------------------------------------| +| [Installation](install.md) | How to install CORE and its requirements | +| [Architecture](architecture.md) | Overview of the architecture | +| [Node Types](nodetypes.md) | Overview of node types supported within CORE | +| [GUI](gui.md) | How to use the GUI | +| [Python API](python.md) | Covers how to control core directly using python | +| [gRPC API](grpc.md) | Covers how control core using gRPC | +| [Distributed](distributed.md) | Details for running CORE across multiple servers | +| [Control Network](ctrlnet.md) | How to use control networks to communicate with nodes from host | +| [Config Services](configservices.md) | Overview of provided config services and creating custom ones | +| [Services](services.md) | Overview of provided services and creating custom ones | +| [EMANE](emane.md) | Overview of EMANE integration and integrating custom EMANE models | +| [Performance](performance.md) | Notes on performance when using CORE | +| [Developers Guide](devguide.md) | Overview on how to contribute to CORE | diff --git a/docs/install.md b/docs/install.md index 51c05dbc..fcc8484e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,20 +1,15 @@ # Installation - -!!! warning - - If Docker is installed, the default iptable rules will block CORE traffic +* Table of Contents +{:toc} ## Overview +CORE provides a script to help automate the installation of dependencies, +build and install, and either generate a CORE specific python virtual environment +or build and install a python wheel. -CORE currently supports and provides the following installation options, with the package -option being preferred. - -* [Package based install (rpm/deb)](#package-based-install) -* [Script based install](#script-based-install) -* [Dockerfile based install](#dockerfile-based-install) +> **WARNING:** if Docker is installed, the default iptable rules will block CORE traffic ### Requirements - Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous containers, as a general rule you should select a machine having as much RAM and CPU resources as possible. @@ -23,36 +18,55 @@ containers, as a general rule you should select a machine having as much RAM and * nftables compatible kernel and nft command line tool ### Supported Linux Distributions - Plan is to support recent Ubuntu and CentOS LTS releases. Verified: +* Ubuntu - 18.04, 20.04 +* CentOS - 7.8, 8.0 -* Ubuntu - 18.04, 20.04, 22.04 -* CentOS - 7.8 +> **NOTE:** CentOS 8 does not have the netem kernel mod available by default + +CentOS 8 Enabled netem: +```shell +sudo yum update +# restart into updated kernel +sudo yum install -y kernel-modules-extra +sudo modprobe sch_netem +``` + +### Tools Used +The following tools will be leveraged during installation: + +| Tool | Description | +|---------------------------------------------|-----------------------------------------------------------------------| +| [pip](https://pip.pypa.io/en/stable/) | used to install pipx | +| [pipx](https://pipxproject.github.io/pipx/) | used to install standalone python tools (invoke, poetry) | +| [invoke](http://www.pyinvoke.org/) | used to run provided tasks (install, uninstall, reinstall, etc) | +| [poetry](https://python-poetry.org/) | used to install python virtual environment or building a python wheel | ### Files +The following is a list of files that would be installed after running the automated installation. -The following is a list of files that would be installed after installation. +> **NOTE:** the default install prefix is /usr/local, but can be changed as noted below -* executables - * `/bin/{vcmd, vnode}` - * can be adjusted using script based install , package will be /usr +* executable files + * `/bin/{core-daemon, core-gui, vcmd, vnoded, etc}` * python files - * virtual environment `/opt/core/venv` - * local install will be local to the python version used - * `python3 -c "import core; print(core.__file__)"` - * scripts {core-daemon, core-cleanup, etc} - * virtualenv `/opt/core/venv/bin` - * local `/usr/local/bin` + * poetry virtual env + * `cd /daemon && poetry env info` + * `~/.cache/pypoetry/virtualenvs/` + * local python install + * default install path for python3 installation of a wheel + * `python3 -c "import core; print(core.__file__)"` * configuration files - * `/etc/core/{core.conf, logging.conf}` -* ospf mdr repository files when using script based install - * `/../ospf-mdr` + * `/etc/core/{core.conf, logging.conf}` +* ospf mdr repository files + * `/../ospf-mdr` +* emane repository files + * `/../emane` -### Installed Scripts - -The following python scripts are provided. +### Installed Executables +After the installation complete it will have installed the following scripts. | Name | Description | |---------------------|------------------------------------------------------------------------------| @@ -64,21 +78,18 @@ The following python scripts are provided. | core-route-monitor | tool to help monitor traffic across nodes and feed that to SDT | | core-service-update | tool to update automate modifying a legacy service to match current naming | -### Upgrading from Older Release - +## Upgrading from Older Release Please make sure to uninstall any previous installations of CORE cleanly before proceeding to install. Clearing out a current install from 7.0.0+, making sure to provide options used for install (`-l` or `-p`). - ```shell cd inv uninstall ``` Previous install was built from source for CORE release older than 7.0.0: - ```shell cd sudo make uninstall @@ -87,7 +98,6 @@ make clean ``` Installed from previously built packages: - ```shell # centos sudo yum remove core @@ -95,144 +105,84 @@ sudo yum remove core sudo apt remove core ``` -## Installation Examples - -The below links will take you to sections providing complete examples for installing -CORE and related utilities on fresh installations. Otherwise, a breakdown for installing -different components and the options available are detailed below. - -* [Ubuntu 22.04](install_ubuntu.md) -* [CentOS 7](install_centos.md) - -## Package Based Install +## Installing from Packages Starting with 9.0.0 there are pre-built rpm/deb packages. You can retrieve the rpm/deb package from [releases](https://github.com/coreemu/core/releases) page. -The built packages will require and install system level dependencies, as well as running -a post install script to install the provided CORE python wheel. A similar uninstall script -is ran when uninstalling and would require the same options as given, during the install. +> **NOTE:** PYTHON defaults to python3 for installs below -!!! note - - PYTHON defaults to python3 for installs below, CORE requires python3.9+, pip, - tk compatibility for python gui, and venv for virtual environments - -Examples for install: +> **NOTE:** the python install requires python3.6+, pip, +> tk compatibility for python gui, and venv for virtualenvs ```shell # recommended to upgrade to the latest version of pip before installation # in python, can help avoid building from source issues -sudo -m pip install --upgrade pip -# install vcmd/vnoded, system dependencies, + -m pip install --upgrade pip + +# install core vcmd/vnoded, system dependencies, # and core python into a venv located at /opt/core/venv -sudo install -y ./ + install -y ./ + # disable the venv and install to python directly -sudo NO_VENV=1 install -y ./ -# change python executable used to install for venv or direct installations -sudo PYTHON=python3.9 install -y ./ +NO_VENV=1 install -y ./ + +# change python executable used to install for venv and direct installations +PYTHON=python3.9 install -y ./ + # disable venv and change python executable -sudo NO_VENV=1 PYTHON=python3.9 install -y ./ +NO_VENV=1 PYTHON=python3.9 install -y ./ + # skip installing the python portion entirely, as you plan to carry this out yourself # core python wheel is located at /opt/core/core--py3-none-any.whl -sudo NO_PYTHON=1 install -y ./ +NO_PYTHON=1 install -y ./ + # install python wheel into python of your choosing -sudo -m pip install /opt/core/core--py3-none-any.whl + -m pip install /opt/core/core--py3-none-any.whl ``` -Example for removal, requires using the same options as install: - -```shell -# remove a standard install -sudo remove core -# remove a local install -sudo NO_VENV=1 remove core -# remove install using alternative python -sudo PYTHON=python3.9 remove core -# remove install using alternative python and local install -sudo NO_VENV=1 PYTHON=python3.9 remove core -# remove install and skip python uninstall -sudo NO_PYTHON=1 remove core -``` - -### Installing OSPF MDR - -You will need to manually install OSPF MDR for routing nodes, since this is not -provided by the package. - -```shell -git clone https://github.com/USNavalResearchLaboratory/ospf-mdr.git -cd ospf-mdr -./bootstrap.sh -./configure --disable-doc --enable-user=root --enable-group=root \ - --with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga -make -j$(nproc) -sudo make install -``` - -When done see [Post Install](#post-install). - -## Script Based Install - -The script based installation will install system level dependencies, python library and -dependencies, as well as dependencies for building CORE. - -The script based install also automatically builds and installs OSPF MDR, used by default -on routing nodes. This can optionally be skipped. - -Installaion will carry out the following steps: - -* installs system dependencies for building core -* builds vcmd/vnoded and python grpc files -* installs core into poetry managed virtual environment or locally, if flag is passed -* installs systemd service pointing to appropriate python location based on install type -* clone/build/install working version of [OPSF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) - -!!! note - - Installing locally comes with its own risks, it can result it potential - dependency conflicts with system package manager installed python dependencies - -!!! note - - Provide a prefix that will be found on path when running as sudo, - if the default prefix /usr/local will not be valid - -The following tools will be leveraged during installation: - -| Tool | Description | -|---------------------------------------------|-----------------------------------------------------------------------| -| [pip](https://pip.pypa.io/en/stable/) | used to install pipx | -| [pipx](https://pipxproject.github.io/pipx/) | used to install standalone python tools (invoke, poetry) | -| [invoke](http://www.pyinvoke.org/) | used to run provided tasks (install, uninstall, reinstall, etc) | -| [poetry](https://python-poetry.org/) | used to install python virtual environment or building a python wheel | - +## Automated Install First we will need to clone and navigate to the CORE repo. - ```shell # clone CORE repo git clone https://github.com/coreemu/core.git cd core - # install dependencies to run installation task ./setup.sh -# skip installing system packages, due to using python built from source -NO_SYSTEM=1 ./setup.sh - # run the following or open a new terminal source ~/.bashrc - # Ubuntu inv install # CentOS inv install -p /usr -# optionally skip python system packages -inv install --no-python -# optionally skip installing ospf mdr -inv install --no-ospf +``` + +First you can use `setup.sh` as a convenience to install tooling for running invoke tasks: + +> **NOTE:** `setup.sh` will attempt to determine your OS by way of `/etc/os-release`, currently it supports +> attempts to install OSs that are debian/redhat like (yum/apt). + +* python3, pip, venv +* pipx 0.16.4 via pip +* invoke 1.4.1 via pipx +* poetry 1.1.12 via pipx + +Then you can run `inv install `: +* installs system dependencies for building core +* installs core into poetry managed virtual environment or locally, if flag is passed +* installs scripts pointing to appropriate python location based on install type +* installs systemd service pointing to appropriate python location based on install type +* clone/build/install working version of [OPSF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) + +> **NOTE:** installing locally comes with its own risks, it can result it potential +> dependency conflicts with system package manager installed python dependencies + +> **NOTE:** provide a prefix that will be found on path when running as sudo, +> if the default prefix /usr/local will not be valid + +```shell +inv -h install -# install command options Usage: inv[oke] [--core-opts] install [--options] [other tasks here ...] Docstring: @@ -242,20 +192,51 @@ Options: -d, --dev install development mode -i STRING, --install-type=STRING used to force an install type, can be one of the following (redhat, debian) -l, --local determines if core will install to local system, default is False - -n, --no-python avoid installing python system dependencies -o, --[no-]ospf disable ospf installation -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local - -v, --verbose + -v, --verbose enable verbose + +# install core to virtual environment +./install.sh -p + +# install core locally +./install.sh -p -l ``` -When done see [Post Install](#post-install). +After installation has completed you should be able to run `core-daemon` and `core-gui`. + +## Using Invoke Tasks +The invoke tool installed by way of pipx provides conveniences for running +CORE tasks to help ensure usage of the create python virtual environment. + +```shell +inv --list + +Available tasks: + + install install core, poetry, scripts, service, and ospf mdr + install-emane install emane python bindings into the core virtual environment + reinstall run the uninstall task, get latest from specified branch, and run install task + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo + uninstall uninstall core, scripts, service, virtual environment, and clean build directory +``` + +### Enabling Service +After installation, the core service is not enabled by default. If you desire to use the +service, run the following commands. + +```shell +sudo systemctl enable core-daemon +sudo systemctl start core-daemon +``` ### Unsupported Linux Distribution - For unsupported OSs you could attempt to do the following to translate an installation to your use case. -* make sure you have python3.9+ with venv support +* make sure you have python3.6+ with venv support * make sure you have python3 invoke available to leverage `/tasks.py` ```shell @@ -265,143 +246,50 @@ an installation to your use case. inv install --dry -v -p -i ``` -## Dockerfile Based Install - -You can leverage one of the provided Dockerfiles, to run and launch CORE within a Docker container. - -Since CORE nodes will leverage software available within the system for a given use case, -make sure to update and build the Dockerfile with desired software. +## Dockerfile Install +You can leverage the provided Dockerfile, to run and launch CORE within a Docker container. ```shell # clone core git clone https://github.com/coreemu/core.git cd core # build image -sudo docker build -t core -f dockerfiles/Dockerfile. . +sudo docker build -t core. -f Dockerfile. . # start container -sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged core +sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged core. # enable xhost access to the root user xhost +local:root # launch core-gui sudo docker exec -it core core-gui ``` -When done see [Post Install](#post-install). +## Running User Scripts +If you create your own python scripts to run CORE directly or using the gRPC +APIs you will need to make sure you are running them within context of the +installed virtual environment. To help support this CORE provides the `core-python` +executable. This executable will allow you to enter CORE's python virtual +environment interpreter or to run a script within it. + +For installations installed to a virtual environment: +```shell +core-python