From e9b83b0d284e3eb25463eb189020925ab57d15cb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:13:39 -0800 Subject: [PATCH 01/21] daemon: refactored how interfaces are configured, updated link edits to allow proper bi-directional support for network to network interfaces, improved and added more unit tests for link add/edit/delete --- daemon/core/api/grpc/server.py | 16 +- daemon/core/emulator/session.py | 84 +++++---- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 16 +- daemon/core/nodes/interface.py | 88 +++++++++ daemon/core/nodes/network.py | 76 +------- daemon/core/nodes/physical.py | 14 +- daemon/tests/test_links.py | 306 ++++++++++++++++++++----------- 8 files changed, 349 insertions(+), 253 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ba20d4ed6..060bc4b61 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -288,17 +288,25 @@ def StartSession( # create links links = [] - asym_links = [] + edit_links = [] + known_links = set() for link in request.session.links: - if link.options.unidirectional: - asym_links.append(link) + iface1 = link.iface1.id if link.iface1 else None + iface2 = link.iface2.id if link.iface2 else None + if link.node1_id < link.node2_id: + link_id = (link.node1_id, iface1, link.node2_id, iface2) else: + link_id = (link.node2_id, iface2, link.node1_id, iface1) + if link_id in known_links: + edit_links.append(link) + else: + known_links.add(link_id) links.append(link) _, exceptions = grpcutils.create_links(session, links) if exceptions: exceptions = [str(x) for x in exceptions] return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) - _, exceptions = grpcutils.edit_links(session, asym_links) + _, exceptions = grpcutils.edit_links(session, edit_links) if exceptions: exceptions = [str(x) for x in exceptions] return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 4f41a5e5d..46f9a04f1 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -276,20 +276,22 @@ def add_link( ptp = self.create_node(PtpNet, start) iface1 = node1.new_iface(ptp, iface1_data) iface2 = node2.new_iface(ptp, iface2_data) - ptp.linkconfig(iface1, options) + iface1.config(options) if not options.unidirectional: - ptp.linkconfig(iface2, options) + iface2.config(options) # link node to net elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + logger.info("linking node to net: %s - %s", node1.name, node2.name) iface1 = node1.new_iface(node2, iface1_data) if not isinstance(node2, (EmaneNet, WlanNode)): - node2.linkconfig(iface1, options) + iface1.config(options) # link net to node elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + logger.info("linking net to node: %s - %s", node1.name, node2.name) iface2 = node2.new_iface(node1, iface2_data) wireless_net = isinstance(node1, (EmaneNet, WlanNode)) if not options.unidirectional and not wireless_net: - node1.linkconfig(iface2, options) + iface2.config(options) # network to network elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase @@ -298,11 +300,10 @@ def add_link( "linking network to network: %s - %s", node1.name, node2.name ) iface1 = node1.linknet(node2) - node1.linkconfig(iface1, options) + use_local = iface1.net == node1 + iface1.config(options, use_local=use_local) if not options.unidirectional: - iface1.swapparams("_params_up") - node2.linkconfig(iface1, options) - iface1.swapparams("_params_up") + iface1.config(options, use_local=not use_local) else: raise CoreError( f"cannot link node1({type(node1)}) node2({type(node2)})" @@ -379,16 +380,18 @@ def delete_link( elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase ): - for iface in node1.get_ifaces(control=False): - if iface.othernet == node2: - node1.detach(iface) - iface.shutdown() - break - for iface in node2.get_ifaces(control=False): - if iface.othernet == node1: - node2.detach(iface) - iface.shutdown() - break + iface1 = node1.get_linked_iface(node2) + if iface1: + node1.detach(iface1) + iface1.shutdown() + iface2 = node2.get_linked_iface(node1) + if iface2: + node2.detach(iface2) + iface2.shutdown() + if not iface1 and not iface2: + raise CoreError( + f"node1({node1.name}) and node2({node2.name}) are not connected" + ) self.sdt.delete_link(node1_id, node2_id) def update_link( @@ -432,11 +435,11 @@ def update_link( else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): iface1 = node1.ifaces.get(iface1_id) - iface2 = node2.ifaces.get(iface2_id) if not iface1: raise CoreError( f"node({node1.name}) missing interface({iface1_id})" ) + iface2 = node2.ifaces.get(iface2_id) if not iface2: raise CoreError( f"node({node2.name}) missing interface({iface2_id})" @@ -446,39 +449,40 @@ def update_link( f"node1({node1.name}) node2({node2.name}) " "not connected to same net" ) - ptp = iface1.net - ptp.linkconfig(iface1, options, iface2) + iface1.config(options) if not options.unidirectional: - ptp.linkconfig(iface2, options, iface1) + iface2.config(options) elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): iface = node1.get_iface(iface1_id) - node2.linkconfig(iface, options) + if iface.net != node2: + raise CoreError( + f"node1({node1.name}) iface1({iface1_id})" + f" is not linked to node1({node2.name})" + ) + iface.config(options) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): iface = node2.get_iface(iface2_id) - node1.linkconfig(iface, options) + if iface.net != node1: + raise CoreError( + f"node2({node2.name}) iface2({iface2_id})" + f" is not linked to node1({node1.name})" + ) + iface.config(options) elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase ): iface = node1.get_linked_iface(node2) - upstream = False if not iface: - upstream = True iface = node2.get_linked_iface(node1) - if not iface: - raise CoreError("modify unknown link between nets") - if upstream: - iface.swapparams("_params_up") - node1.linkconfig(iface, options) - iface.swapparams("_params_up") + if iface: + use_local = iface.net == node1 + iface.config(options, use_local=use_local) + if not options.unidirectional: + iface.config(options, use_local=not use_local) else: - node1.linkconfig(iface, options) - if not options.unidirectional: - if upstream: - node2.linkconfig(iface, options) - else: - iface.swapparams("_params_up") - node2.linkconfig(iface, options) - iface.swapparams("_params_up") + raise CoreError( + f"node1({node1.name}) and node2({node2.name}) are not linked" + ) else: raise CoreError( f"cannot update link node1({type(node1)}) node2({type(node2)})" diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 4c61c0651..0464ebc7b 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -321,7 +321,7 @@ def setlinkparams(self) -> None: loss=self.loss, jitter=self.jitter, ) - self.wlan.linkconfig(iface, options) + iface.config(options) def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 91c1fdcc3..ca6a43efd 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -13,7 +13,7 @@ from core import utils from core.configservice.dependencies import ConfigServiceDependencies -from core.emulator.data import InterfaceData, LinkData, LinkOptions +from core.emulator.data import InterfaceData, LinkData from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.executables import MOUNT, TEST, VNODED @@ -1000,20 +1000,6 @@ def linknet(self, net: "CoreNetworkBase") -> CoreInterface: """ raise NotImplementedError - @abc.abstractmethod - def linkconfig( - self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None - ) -> None: - """ - Configure link parameters by applying tc queuing disciplines on the interface. - - :param iface: interface one - :param options: options for configuring link - :param iface2: interface two - :return: nothing - """ - raise NotImplementedError - def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: raise NotImplementedError diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 7fda18c78..7fc0a3869 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -3,6 +3,7 @@ """ import logging +import math import time from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple @@ -13,6 +14,7 @@ from core.emulator.data import LinkOptions from core.emulator.enumerations import TransportType from core.errors import CoreCommandError, CoreError +from core.executables import TC from core.nodes.netclient import LinuxNetClient, get_net_client logger = logging.getLogger(__name__) @@ -336,6 +338,92 @@ def is_virtual(self) -> bool: """ return self.transport_type == TransportType.VIRTUAL + def _set_params_change(self, **kwargs: float) -> bool: + """ + Set parameters to change. + + :param kwargs: parameter name and values to change + :return: True if any parameter changed, False otherwise + """ + return any([self.setparam(k, v) for k, v in kwargs.items()]) + + def config(self, options: LinkOptions, use_local: bool = True) -> None: + """ + Configure interface using tc based on existing state and provided + link options. + + :param options: options to configure with + :param use_local: True to use localname for device, False for name + :return: nothing + """ + # determine if any settings have changed + if use_local: + devname = self.localname + changed = self._set_params_change( + bw=options.bandwidth, + delay=options.delay, + loss=options.loss, + duplicate=options.dup, + jitter=options.jitter, + buffer=options.buffer, + ) + else: + devname = self.name + changed = self._set_params_change( + n_bw=options.bandwidth, + n_delay=options.delay, + n_loss=options.loss, + n_duplicate=options.dup, + n_jitter=options.jitter, + n_buffer=options.buffer, + ) + if not changed: + return + # delete tc configuration or create and add it + if all( + [ + options.delay is None or options.delay <= 0, + options.jitter is None or options.jitter <= 0, + options.loss is None or options.loss <= 0, + options.dup is None or options.dup <= 0, + options.bandwidth is None or options.bandwidth <= 0, + options.buffer is None or options.buffer <= 0, + ] + ): + if not self.getparam("has_netem"): + return + if self.up: + cmd = f"{TC} qdisc delete dev {devname} root handle 10:" + self.host_cmd(cmd) + self.setparam("has_netem", False) + else: + netem = "" + if options.bandwidth is not None: + limit = 1000 + bw = options.bandwidth / 1000 + if options.buffer is not None and options.buffer > 0: + limit = options.buffer + elif options.delay and options.bandwidth: + delay = options.delay / 1000 + limit = max(2, math.ceil((2 * bw * delay) / (8 * self.mtu))) + netem += f" rate {bw}kbit" + netem += f" limit {limit}" + if options.delay is not None: + netem += f" delay {options.delay}us" + if options.jitter is not None: + if options.delay is None: + netem += f" delay 0us {options.jitter}us 25%" + else: + netem += f" {options.jitter}us 25%" + if options.loss is not None and options.loss > 0: + netem += f" loss {min(options.loss, 100)}%" + if options.dup is not None and options.dup > 0: + netem += f" duplicate {min(options.dup, 100)}%" + if self.up: + cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" + self.host_cmd(cmd) + self.setparam("has_netem", True) + class Veth(CoreInterface): """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 34f0f878a..2128c9b60 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -3,7 +3,6 @@ """ import logging -import math import threading import time from collections import OrderedDict @@ -14,7 +13,7 @@ import netaddr from core import utils -from core.emulator.data import InterfaceData, LinkData, LinkOptions +from core.emulator.data import InterfaceData, LinkData from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -23,7 +22,7 @@ RegisterTlvs, ) from core.errors import CoreCommandError, CoreError -from core.executables import NFTABLES, TC +from core.executables import NFTABLES from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.netclient import get_net_client @@ -400,77 +399,6 @@ def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: self.linked[iface1][iface2] = True nft_queue.update(self) - def linkconfig( - self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None - ) -> None: - """ - Configure link parameters by applying tc queuing disciplines on the interface. - - :param iface: interface one - :param options: options for configuring link - :param iface2: interface two - :return: nothing - """ - # determine if any settings have changed - changed = any( - [ - iface.setparam("bw", options.bandwidth), - iface.setparam("delay", options.delay), - iface.setparam("loss", options.loss), - iface.setparam("duplicate", options.dup), - iface.setparam("jitter", options.jitter), - iface.setparam("buffer", options.buffer), - ] - ) - if not changed: - return - - # delete tc configuration or create and add it - devname = iface.localname - if all( - [ - options.delay is None or options.delay <= 0, - options.jitter is None or options.jitter <= 0, - options.loss is None or options.loss <= 0, - options.dup is None or options.dup <= 0, - options.bandwidth is None or options.bandwidth <= 0, - options.buffer is None or options.buffer <= 0, - ] - ): - if not iface.getparam("has_netem"): - return - if self.up: - cmd = f"{TC} qdisc delete dev {devname} root handle 10:" - iface.host_cmd(cmd) - iface.setparam("has_netem", False) - else: - netem = "" - if options.bandwidth is not None: - limit = 1000 - bw = options.bandwidth / 1000 - if options.buffer is not None and options.buffer > 0: - limit = options.buffer - elif options.delay and options.bandwidth: - delay = options.delay / 1000 - limit = max(2, math.ceil((2 * bw * delay) / (8 * iface.mtu))) - netem += f" rate {bw}kbit" - netem += f" limit {limit}" - if options.delay is not None: - netem += f" delay {options.delay}us" - if options.jitter is not None: - if options.delay is None: - netem += f" delay 0us {options.jitter}us 25%" - else: - netem += f" {options.jitter}us 25%" - if options.loss is not None and options.loss > 0: - netem += f" loss {min(options.loss, 100)}%" - if options.dup is not None and options.dup > 0: - netem += f" duplicate {min(options.dup, 100)}%" - if self.up: - cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" - iface.host_cmd(cmd) - iface.setparam("has_netem", True) - def linknet(self, net: CoreNetworkBase) -> CoreInterface: """ Link this bridge with another by creating a veth pair and installing diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index dab2a954f..4dd5fb571 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -7,14 +7,13 @@ from pathlib import Path from typing import TYPE_CHECKING, List, Optional, Tuple -from core.emulator.data import InterfaceData, LinkOptions +from core.emulator.data import InterfaceData from core.emulator.distributed import DistributedServer from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError from core.executables import MOUNT, TEST, UMOUNT from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface -from core.nodes.network import CoreNetwork logger = logging.getLogger(__name__) @@ -143,17 +142,6 @@ def adopt_iface( if self.up: self.net_client.device_up(iface.localname) - def linkconfig( - self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None - ) -> None: - """ - Apply tc queing disciplines using linkconfig. - """ - linux_bridge = CoreNetwork(self.session) - linux_bridge.up = True - linux_bridge.linkconfig(iface, options, iface2) - del linux_bridge - def next_iface_id(self) -> int: with self.lock: while self.iface_id in self.ifaces: diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 94c8c6997..1d9b54cef 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -1,10 +1,19 @@ from typing import Tuple +import pytest + from core.emulator.data import IpPrefixes, LinkOptions from core.emulator.session import Session +from core.errors import CoreError from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.nodes.network import SwitchNode +INVALID_ID: int = 100 +LINK_OPTIONS: LinkOptions = LinkOptions( + delay=50, bandwidth=5000000, loss=25, dup=25, jitter=10, buffer=100 +) + def create_ptp_network( session: Session, ip_prefixes: IpPrefixes @@ -24,8 +33,28 @@ def create_ptp_network( return node1, node2 +def check_iface_match(iface: CoreInterface, options: LinkOptions) -> bool: + result = iface.getparam("delay") == options.delay + result &= iface.getparam("bw") == options.bandwidth + result &= iface.getparam("loss") == options.loss + result &= iface.getparam("duplicate") == options.dup + result &= iface.getparam("jitter") == options.jitter + result &= iface.getparam("buffer") == options.buffer + return result + + +def check_iface_diff(iface: CoreInterface, options: LinkOptions) -> bool: + result = iface.getparam("delay") != options.delay + result &= iface.getparam("bw") != options.bandwidth + result &= iface.getparam("loss") != options.loss + result &= iface.getparam("duplicate") != options.dup + result &= iface.getparam("jitter") != options.jitter + result &= iface.getparam("buffer") != options.buffer + return result + + class TestLinks: - def test_add_ptp(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) @@ -33,11 +62,17 @@ def test_add_ptp(self, session: Session, ip_prefixes: IpPrefixes): iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node1.id, node2.id, iface1_data, iface2_data) + iface1, iface2 = session.add_link( + node1.id, node2.id, iface1_data, iface2_data, options=LINK_OPTIONS + ) # then assert node1.get_iface(iface1_data.id) assert node2.get_iface(iface2_data.id) + assert iface1 is not None + assert iface2 is not None + assert check_iface_match(iface1, LINK_OPTIONS) + assert check_iface_match(iface2, LINK_OPTIONS) def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -46,11 +81,15 @@ def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): iface1_data = ip_prefixes.create_iface(node1) # when - session.add_link(node1.id, node2.id, iface1_data=iface1_data) + iface, _ = session.add_link( + node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS + ) # then assert node2.links() assert node1.get_iface(iface1_data.id) + assert iface is not None + assert check_iface_match(iface, LINK_OPTIONS) def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -59,11 +98,15 @@ def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node1.id, node2.id, iface2_data=iface2_data) + _, iface = session.add_link( + node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS + ) # then assert node1.links() assert node2.get_iface(iface2_data.id) + assert iface is not None + assert check_iface_match(iface, LINK_OPTIONS) def test_add_net_to_net(self, session): # given @@ -71,147 +114,119 @@ def test_add_net_to_net(self, session): node2 = session.add_node(SwitchNode) # when - session.add_link(node1.id, node2.id) + iface, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) # then assert node1.links() + assert iface is not None + assert check_iface_match(iface, LINK_OPTIONS) + + def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + link_options1 = LinkOptions( + delay=50, + bandwidth=5000000, + loss=25, + dup=25, + jitter=10, + buffer=100, + unidirectional=True, + ) + link_options2 = LinkOptions( + delay=51, + bandwidth=5000001, + loss=26, + dup=26, + jitter=11, + buffer=101, + unidirectional=True, + ) + + # when + iface1, iface2 = session.add_link( + node1.id, node2.id, iface1_data, iface2_data, link_options1 + ) + session.update_link( + node2.id, node1.id, iface2_data.id, iface1_data.id, link_options2 + ) + + # then + assert node1.get_iface(iface1_data.id) + assert node2.get_iface(iface2_data.id) + assert iface1 is not None + assert iface2 is not None + assert check_iface_match(iface1, link_options1) + assert check_iface_match(iface2, link_options2) def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given - delay = 50 - bandwidth = 5000000 - loss = 25 - dup = 25 - jitter = 10 - buffer = 100 node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) - session.add_link(node1.id, node2.id, iface1_data) - iface1 = node1.get_iface(iface1_data.id) - assert iface1.getparam("delay") != delay - assert iface1.getparam("bw") != bandwidth - assert iface1.getparam("loss") != loss - assert iface1.getparam("duplicate") != dup - assert iface1.getparam("jitter") != jitter - assert iface1.getparam("buffer") != buffer + iface1, _ = session.add_link(node1.id, node2.id, iface1_data) + assert check_iface_diff(iface1, LINK_OPTIONS) # when - options = LinkOptions( - delay=delay, - bandwidth=bandwidth, - loss=loss, - dup=dup, - jitter=jitter, - buffer=buffer, - ) session.update_link( - node1.id, node2.id, iface1_id=iface1_data.id, options=options + node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS ) # then - assert iface1.getparam("delay") == delay - assert iface1.getparam("bw") == bandwidth - assert iface1.getparam("loss") == loss - assert iface1.getparam("duplicate") == dup - assert iface1.getparam("jitter") == jitter - assert iface1.getparam("buffer") == buffer + assert check_iface_match(iface1, LINK_OPTIONS) def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given - delay = 50 - bandwidth = 5000000 - loss = 25 - dup = 25 - jitter = 10 - buffer = 100 node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) - session.add_link(node1.id, node2.id, iface2_data=iface2_data) - iface2 = node2.get_iface(iface2_data.id) - assert iface2.getparam("delay") != delay - assert iface2.getparam("bw") != bandwidth - assert iface2.getparam("loss") != loss - assert iface2.getparam("duplicate") != dup - assert iface2.getparam("jitter") != jitter - assert iface2.getparam("buffer") != buffer + _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert check_iface_diff(iface2, LINK_OPTIONS) # when - options = LinkOptions( - delay=delay, - bandwidth=bandwidth, - loss=loss, - dup=dup, - jitter=jitter, - buffer=buffer, - ) session.update_link( - node1.id, node2.id, iface2_id=iface2_data.id, options=options + node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS ) # then - assert iface2.getparam("delay") == delay - assert iface2.getparam("bw") == bandwidth - assert iface2.getparam("loss") == loss - assert iface2.getparam("duplicate") == dup - assert iface2.getparam("jitter") == jitter - assert iface2.getparam("buffer") == buffer + assert check_iface_match(iface2, LINK_OPTIONS) def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given - delay = 50 - bandwidth = 5000000 - loss = 25 - dup = 25 - jitter = 10 - buffer = 100 node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) - session.add_link(node1.id, node2.id, iface1_data, iface2_data) - iface1 = node1.get_iface(iface1_data.id) - iface2 = node2.get_iface(iface2_data.id) - assert iface1.getparam("delay") != delay - assert iface1.getparam("bw") != bandwidth - assert iface1.getparam("loss") != loss - assert iface1.getparam("duplicate") != dup - assert iface1.getparam("jitter") != jitter - assert iface1.getparam("buffer") != buffer - assert iface2.getparam("delay") != delay - assert iface2.getparam("bw") != bandwidth - assert iface2.getparam("loss") != loss - assert iface2.getparam("duplicate") != dup - assert iface2.getparam("jitter") != jitter - assert iface2.getparam("buffer") != buffer + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert check_iface_diff(iface1, LINK_OPTIONS) + assert check_iface_diff(iface2, LINK_OPTIONS) # when - options = LinkOptions( - delay=delay, - bandwidth=bandwidth, - loss=loss, - dup=dup, - jitter=jitter, - buffer=buffer, + session.update_link( + node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS ) - session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options) # then - assert iface1.getparam("delay") == delay - assert iface1.getparam("bw") == bandwidth - assert iface1.getparam("loss") == loss - assert iface1.getparam("duplicate") == dup - assert iface1.getparam("jitter") == jitter - assert iface1.getparam("buffer") == buffer - assert iface2.getparam("delay") == delay - assert iface2.getparam("bw") == bandwidth - assert iface2.getparam("loss") == loss - assert iface2.getparam("duplicate") == dup - assert iface2.getparam("jitter") == jitter - assert iface2.getparam("buffer") == buffer - - def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): + assert check_iface_match(iface1, LINK_OPTIONS) + assert check_iface_match(iface2, LINK_OPTIONS) + + def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + iface1, _ = session.add_link(node1.id, node2.id) + assert check_iface_diff(iface1, LINK_OPTIONS) + + # when + session.update_link(node1.id, node2.id, options=LINK_OPTIONS) + + # then + assert check_iface_match(iface1, LINK_OPTIONS) + + def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) @@ -255,3 +270,82 @@ def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # then assert iface2_data.id not in node2.ifaces + + def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + session.add_link(node1.id, node2.id) + assert node1.get_linked_iface(node2) + + # when + session.delete_link(node1.id, node2.id) + + # then + assert not node1.get_linked_iface(node2) + + def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + session.add_link(node1.id, node2.id) + assert node1.get_linked_iface(node2) + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, INVALID_ID) + with pytest.raises(CoreError): + session.delete_link(INVALID_ID, node2.id) + + def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + node3 = session.add_node(SwitchNode) + session.add_link(node1.id, node2.id) + assert node1.get_linked_iface(node2) + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, node3.id) + + def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + node3 = session.add_node(SwitchNode) + iface1_data = ip_prefixes.create_iface(node1) + iface1, _ = session.add_link(node1.id, node2.id, iface1_data) + assert iface1 + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, node3.id) + + def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + node3 = session.add_node(SwitchNode) + iface2_data = ip_prefixes.create_iface(node2) + _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert iface2 + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, node3.id) + + def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + node3 = session.add_node(SwitchNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert iface1 + assert iface2 + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, node3.id) From 6791269eebc15aa25e0a05a957cea9d085b7103a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 Jan 2022 21:39:52 -0800 Subject: [PATCH 02/21] daemon: refactored interfaces to store configuration options as link options, instead of using a dictionary --- daemon/core/emulator/data.py | 63 +++++++- daemon/core/nodes/base.py | 59 +++----- daemon/core/nodes/interface.py | 256 +++++++++++++-------------------- daemon/core/nodes/network.py | 49 ++----- daemon/tests/test_core.py | 6 - daemon/tests/test_links.py | 91 +++++++----- 6 files changed, 238 insertions(+), 286 deletions(-) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 4ce92d0b7..28dcb813e 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, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, List, Optional, Tuple import netaddr @@ -176,6 +176,67 @@ class LinkOptions: key: int = None buffer: int = None + def update(self, options: "LinkOptions") -> bool: + """ + Updates current options with values from other options. + + :param options: options to update with + :return: True if any value has changed, False otherwise + """ + changed = False + if options.delay is not None and 0 <= options.delay != self.delay: + self.delay = options.delay + changed = True + if options.bandwidth is not None and 0 <= options.bandwidth != self.bandwidth: + self.bandwidth = options.bandwidth + changed = True + if options.loss is not None and 0 <= options.loss != self.loss: + self.loss = options.loss + changed = True + if options.dup is not None and 0 <= options.dup != self.dup: + self.dup = options.dup + changed = True + if options.jitter is not None and 0 <= options.jitter != self.jitter: + self.jitter = options.jitter + changed = True + if options.buffer is not None and 0 <= options.buffer != self.buffer: + self.buffer = options.buffer + changed = True + return changed + + def is_clear(self) -> bool: + """ + Checks if the current option values represent a clear state. + + :return: True if the current values should clear, False otherwise + """ + clear = self.delay is None or self.delay <= 0 + clear &= self.jitter is None or self.jitter <= 0 + clear &= self.loss is None or self.loss <= 0 + clear &= self.dup is None or self.dup <= 0 + clear &= self.bandwidth is None or self.bandwidth <= 0 + clear &= self.buffer is None or self.buffer <= 0 + return clear + + def __eq__(self, other: Any) -> bool: + """ + Custom logic to check if this link options is equivalent to another. + + :param other: other object to check + :return: True if they are both link options with the same values, + False otherwise + """ + if not isinstance(other, LinkOptions): + return False + return ( + self.delay == other.delay + and self.jitter == other.jitter + and self.loss == other.loss + and self.dup == other.dup + and self.bandwidth == other.bandwidth + and self.buffer == other.buffer + ) + @dataclass class LinkData: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index ca6a43efd..03979d012 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1049,11 +1049,10 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: :return: list of link data """ all_links = [] - # build a link message from this network node to each node having a # connected interface for iface in self.get_ifaces(): - uni = False + unidirectional = 0 linked_node = iface.node if linked_node is None: # two layer-2 switches/hubs linked together via linknet() @@ -1062,53 +1061,29 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: linked_node = iface.othernet if linked_node.id == self.id: continue - iface.swapparams("_params_up") - upstream_params = iface.getparams() - iface.swapparams("_params_up") - if iface.getparams() != upstream_params: - uni = True - - unidirectional = 0 - if uni: - unidirectional = 1 - - mac = str(iface.mac) if iface.mac else None - iface2_data = InterfaceData( - id=linked_node.get_iface_id(iface), name=iface.name, mac=mac - ) - ip4 = iface.get_ip4() - if ip4: - iface2_data.ip4 = str(ip4.ip) - iface2_data.ip4_mask = ip4.prefixlen - ip6 = iface.get_ip6() - if ip6: - iface2_data.ip6 = str(ip6.ip) - iface2_data.ip6_mask = ip6.prefixlen - - options_data = iface.get_link_options(unidirectional) + if iface.local_options != iface.options: + unidirectional = 1 + iface_data = iface.get_data() link_data = LinkData( message_type=flags, type=self.linktype, node1_id=self.id, node2_id=linked_node.id, - iface2=iface2_data, - options=options_data, - ) - all_links.append(link_data) - - if not uni: - continue - iface.swapparams("_params_up") - options_data = iface.get_link_options(unidirectional) - link_data = LinkData( - message_type=MessageFlags.NONE, - type=self.linktype, - node1_id=linked_node.id, - node2_id=self.id, - options=options_data, + iface2=iface_data, + options=iface.local_options, ) - iface.swapparams("_params_up") + link_data.options.unidirectional = unidirectional all_links.append(link_data) + if unidirectional: + link_data = LinkData( + message_type=MessageFlags.NONE, + type=self.linktype, + node1_id=linked_node.id, + node2_id=self.id, + options=iface.options, + ) + link_data.options.unidirectional = unidirectional + all_links.append(link_data) return all_links diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 7fc0a3869..70eb679f4 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -6,12 +6,12 @@ import math import time from pathlib import Path -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional import netaddr from core import utils -from core.emulator.data import LinkOptions +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.enumerations import TransportType from core.errors import CoreCommandError, CoreError from core.executables import TC @@ -27,6 +27,50 @@ DEFAULT_MTU: int = 1500 +def tc_clear_cmd(name: str) -> str: + """ + Create tc command to clear device configuration. + + :param name: name of device to clear + :return: tc command + """ + return f"{TC} qdisc delete dev {name} root handle 10:" + + +def tc_cmd(name: str, options: LinkOptions, mtu: int) -> str: + """ + Create tc command to configure a device with given name and options. + + :param name: name of device to configure + :param options: options to configure with + :param mtu: mtu for configuration + :return: tc command + """ + netem = "" + if options.bandwidth is not None: + limit = 1000 + bw = options.bandwidth / 1000 + if options.buffer is not None and options.buffer > 0: + limit = options.buffer + elif options.delay and options.bandwidth: + delay = options.delay / 1000 + limit = max(2, math.ceil((2 * bw * delay) / (8 * mtu))) + netem += f" rate {bw}kbit" + netem += f" limit {limit}" + if options.delay is not None: + netem += f" delay {options.delay}us" + if options.jitter is not None: + if options.delay is None: + netem += f" delay 0us {options.jitter}us 25%" + else: + netem += f" {options.jitter}us 25%" + if options.loss is not None and options.loss > 0: + netem += f" loss {min(options.loss, 100)}%" + if options.dup is not None and options.dup > 0: + netem += f" duplicate {min(options.dup, 100)}%" + return f"{TC} qdisc replace dev {name} root handle 10: netem {netem}" + + class CoreInterface: """ Base class for network interfaces. @@ -63,7 +107,6 @@ def __init__( self.mtu: int = mtu self.net: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None - self._params: Dict[str, float] = {} self.ip4s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = [] self.mac: Optional[netaddr.EUI] = None @@ -82,6 +125,11 @@ def __init__( self.session.use_ovs(), self.host_cmd ) self.control: bool = False + # configuration data + self.has_local_netem: bool = False + self.local_options: LinkOptions = LinkOptions() + self.has_netem: bool = False + self.options: LinkOptions = LinkOptions() def host_cmd( self, @@ -221,89 +269,6 @@ def set_mac(self, mac: Optional[str]) -> None: except netaddr.AddrFormatError as e: raise CoreError(f"invalid mac address({mac}): {e}") - def getparam(self, key: str) -> float: - """ - Retrieve a parameter from the, or None if the parameter does not exist. - - :param key: parameter to get value for - :return: parameter value - """ - return self._params.get(key) - - def get_link_options(self, unidirectional: int) -> LinkOptions: - """ - Get currently set params as link options. - - :param unidirectional: unidirectional setting - :return: link options - """ - delay = self.getparam("delay") - if delay is not None: - delay = int(delay) - bandwidth = self.getparam("bw") - if bandwidth is not None: - bandwidth = int(bandwidth) - dup = self.getparam("duplicate") - if dup is not None: - dup = int(dup) - jitter = self.getparam("jitter") - if jitter is not None: - jitter = int(jitter) - buffer = self.getparam("buffer") - if buffer is not None: - buffer = int(buffer) - return LinkOptions( - delay=delay, - bandwidth=bandwidth, - dup=dup, - jitter=jitter, - loss=self.getparam("loss"), - buffer=buffer, - unidirectional=unidirectional, - ) - - def getparams(self) -> List[Tuple[str, float]]: - """ - Return (key, value) pairs for parameters. - """ - parameters = [] - for k in sorted(self._params.keys()): - parameters.append((k, self._params[k])) - return parameters - - def setparam(self, key: str, value: float) -> bool: - """ - Set a parameter value, returns True if the parameter has changed. - - :param key: parameter name to set - :param value: parameter value - :return: True if parameter changed, False otherwise - """ - # treat None and 0 as unchanged values - logger.debug("setting param: %s - %s", key, value) - if value is None or value < 0: - return False - current_value = self._params.get(key) - if current_value is not None and current_value == value: - return False - self._params[key] = value - return True - - def swapparams(self, name: str) -> None: - """ - Swap out parameters dict for name. If name does not exist, - intialize it. This is for supporting separate upstream/downstream - parameters when two layer-2 nodes are linked together. - - :param name: name of parameter to swap - :return: nothing - """ - tmp = self._params - if not hasattr(self, name): - setattr(self, name, {}) - self._params = getattr(self, name) - setattr(self, name, tmp) - def setposition(self) -> None: """ Dispatch position hook handler when possible. @@ -338,15 +303,6 @@ def is_virtual(self) -> bool: """ return self.transport_type == TransportType.VIRTUAL - def _set_params_change(self, **kwargs: float) -> bool: - """ - Set parameters to change. - - :param kwargs: parameter name and values to change - :return: True if any parameter changed, False otherwise - """ - return any([self.setparam(k, v) for k, v in kwargs.items()]) - def config(self, options: LinkOptions, use_local: bool = True) -> None: """ Configure interface using tc based on existing state and provided @@ -356,73 +312,55 @@ def config(self, options: LinkOptions, use_local: bool = True) -> None: :param use_local: True to use localname for device, False for name :return: nothing """ - # determine if any settings have changed - if use_local: - devname = self.localname - changed = self._set_params_change( - bw=options.bandwidth, - delay=options.delay, - loss=options.loss, - duplicate=options.dup, - jitter=options.jitter, - buffer=options.buffer, - ) - else: - devname = self.name - changed = self._set_params_change( - n_bw=options.bandwidth, - n_delay=options.delay, - n_loss=options.loss, - n_duplicate=options.dup, - n_jitter=options.jitter, - n_buffer=options.buffer, - ) - if not changed: + # determine name, options, and if anything has changed + name = self.localname if use_local else self.name + current_options = self.local_options if use_local else self.options + changed = current_options.update(options) + # nothing more to do when nothing has changed or not up + if not changed or not self.up: return - # delete tc configuration or create and add it - if all( - [ - options.delay is None or options.delay <= 0, - options.jitter is None or options.jitter <= 0, - options.loss is None or options.loss <= 0, - options.dup is None or options.dup <= 0, - options.bandwidth is None or options.bandwidth <= 0, - options.buffer is None or options.buffer <= 0, - ] - ): - if not self.getparam("has_netem"): - return - if self.up: - cmd = f"{TC} qdisc delete dev {devname} root handle 10:" + # clear current settings + if current_options.is_clear(): + clear_local_netem = use_local and self.has_local_netem + clear_netem = not use_local and self.has_netem + if clear_local_netem or clear_netem: + cmd = tc_clear_cmd(name) self.host_cmd(cmd) - self.setparam("has_netem", False) - else: - netem = "" - if options.bandwidth is not None: - limit = 1000 - bw = options.bandwidth / 1000 - if options.buffer is not None and options.buffer > 0: - limit = options.buffer - elif options.delay and options.bandwidth: - delay = options.delay / 1000 - limit = max(2, math.ceil((2 * bw * delay) / (8 * self.mtu))) - netem += f" rate {bw}kbit" - netem += f" limit {limit}" - if options.delay is not None: - netem += f" delay {options.delay}us" - if options.jitter is not None: - if options.delay is None: - netem += f" delay 0us {options.jitter}us 25%" + if use_local: + self.has_local_netem = False else: - netem += f" {options.jitter}us 25%" - if options.loss is not None and options.loss > 0: - netem += f" loss {min(options.loss, 100)}%" - if options.dup is not None and options.dup > 0: - netem += f" duplicate {min(options.dup, 100)}%" - if self.up: - cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" - self.host_cmd(cmd) - self.setparam("has_netem", True) + self.has_netem = False + # set updated settings + else: + cmd = tc_cmd(name, current_options, self.mtu) + self.host_cmd(cmd) + if use_local: + self.has_local_netem = True + else: + self.has_netem = True + + def get_data(self) -> InterfaceData: + """ + Retrieve the data representation of this interface. + + :return: interface data + """ + if self.node: + iface_id = self.node.get_iface_id(self) + else: + iface_id = self.othernet.get_iface_id(self) + data = InterfaceData( + id=iface_id, name=self.name, mac=str(self.mac) if self.mac else None + ) + ip4 = self.get_ip4() + if ip4: + data.ip4 = str(ip4.ip) + data.ip4_mask = ip4.prefixlen + ip6 = self.get_ip6() + if ip6: + data.ip6 = str(ip6.ip) + data.ip6_mask = ip6.prefixlen + return data class Veth(CoreInterface): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 2128c9b60..32d420dd2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -743,41 +743,12 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: all_links = [] if len(self.ifaces) != 2: return all_links - ifaces = self.get_ifaces() iface1 = ifaces[0] iface2 = ifaces[1] - unidirectional = 0 - if iface1.getparams() != iface2.getparams(): - unidirectional = 1 - - mac = str(iface1.mac) if iface1.mac else None - iface1_data = InterfaceData( - id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=mac - ) - ip4 = iface1.get_ip4() - if ip4: - iface1_data.ip4 = str(ip4.ip) - iface1_data.ip4_mask = ip4.prefixlen - ip6 = iface1.get_ip6() - if ip6: - iface1_data.ip6 = str(ip6.ip) - iface1_data.ip6_mask = ip6.prefixlen - - mac = str(iface2.mac) if iface2.mac else None - iface2_data = InterfaceData( - id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=mac - ) - ip4 = iface2.get_ip4() - if ip4: - iface2_data.ip4 = str(ip4.ip) - iface2_data.ip4_mask = ip4.prefixlen - ip6 = iface2.get_ip6() - if ip6: - iface2_data.ip6 = str(ip6.ip) - iface2_data.ip6_mask = ip6.prefixlen - - options_data = iface1.get_link_options(unidirectional) + unidirectional = 0 if iface1.local_options == iface2.local_options else 1 + iface1_data = iface1.get_data() + iface2_data = iface2.get_data() link_data = LinkData( message_type=flags, type=self.linktype, @@ -785,25 +756,23 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: node2_id=iface2.node.id, iface1=iface1_data, iface2=iface2_data, - options=options_data, + options=iface1.local_options, ) + link_data.options.unidirectional = unidirectional all_links.append(link_data) - # build a 2nd link message for the upstream link parameters # (swap if1 and if2) if unidirectional: - iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2)) - iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1)) - options_data = iface2.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, type=self.linktype, node1_id=iface2.node.id, node2_id=iface1.node.id, - iface1=iface1_data, - iface2=iface2_data, - options=options_data, + iface1=InterfaceData(id=iface2_data.id), + iface2=InterfaceData(id=iface1_data.id), + options=iface2.local_options, ) + link_data.options.unidirectional = unidirectional all_links.append(link_data) return all_links diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 1342861b5..d7a834520 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -130,12 +130,6 @@ def test_iface(self, session: Session, ip_prefixes: IpPrefixes): assert 0 in node1.ifaces assert 0 in node2.ifaces - # check interface parameters - iface = node1.get_iface(0) - iface.setparam("test", 1) - assert iface.getparam("test") == 1 - assert iface.getparams() - # delete interface and test that if no longer exists node1.delete_iface(0) assert 0 not in node1.ifaces diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 1d9b54cef..791eb77a6 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -6,7 +6,6 @@ from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode -from core.nodes.interface import CoreInterface from core.nodes.network import SwitchNode INVALID_ID: int = 100 @@ -33,26 +32,6 @@ def create_ptp_network( return node1, node2 -def check_iface_match(iface: CoreInterface, options: LinkOptions) -> bool: - result = iface.getparam("delay") == options.delay - result &= iface.getparam("bw") == options.bandwidth - result &= iface.getparam("loss") == options.loss - result &= iface.getparam("duplicate") == options.dup - result &= iface.getparam("jitter") == options.jitter - result &= iface.getparam("buffer") == options.buffer - return result - - -def check_iface_diff(iface: CoreInterface, options: LinkOptions) -> bool: - result = iface.getparam("delay") != options.delay - result &= iface.getparam("bw") != options.bandwidth - result &= iface.getparam("loss") != options.loss - result &= iface.getparam("duplicate") != options.dup - result &= iface.getparam("jitter") != options.jitter - result &= iface.getparam("buffer") != options.buffer - return result - - class TestLinks: def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -71,8 +50,10 @@ def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): assert node2.get_iface(iface2_data.id) assert iface1 is not None assert iface2 is not None - assert check_iface_match(iface1, LINK_OPTIONS) - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -89,7 +70,8 @@ def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): assert node2.links() assert node1.get_iface(iface1_data.id) assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.has_local_netem def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -106,7 +88,8 @@ def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): assert node1.links() assert node2.get_iface(iface2_data.id) assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.has_local_netem def test_add_net_to_net(self, session): # given @@ -119,7 +102,10 @@ def test_add_net_to_net(self, session): # then assert node1.links() assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.options == LINK_OPTIONS + assert iface.has_local_netem + assert iface.has_netem def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -159,8 +145,10 @@ def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): assert node2.get_iface(iface2_data.id) assert iface1 is not None assert iface2 is not None - assert check_iface_match(iface1, link_options1) - assert check_iface_match(iface2, link_options2) + assert iface1.local_options == link_options1 + assert iface1.has_local_netem + assert iface2.local_options == link_options2 + assert iface2.has_local_netem def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -168,7 +156,7 @@ def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) iface1, _ = session.add_link(node1.id, node2.id, iface1_data) - assert check_iface_diff(iface1, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS # when session.update_link( @@ -176,7 +164,8 @@ def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): ) # then - assert check_iface_match(iface1, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -184,7 +173,7 @@ def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) - assert check_iface_diff(iface2, LINK_OPTIONS) + assert iface2.local_options != LINK_OPTIONS # when session.update_link( @@ -192,7 +181,8 @@ def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): ) # then - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -201,8 +191,8 @@ def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) - assert check_iface_diff(iface1, LINK_OPTIONS) - assert check_iface_diff(iface2, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS + assert iface2.local_options != LINK_OPTIONS # when session.update_link( @@ -210,21 +200,46 @@ def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): ) # then - assert check_iface_match(iface1, LINK_OPTIONS) - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) iface1, _ = session.add_link(node1.id, node2.id) - assert check_iface_diff(iface1, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS # when session.update_link(node1.id, node2.id, options=LINK_OPTIONS) # then - assert check_iface_match(iface1, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + + def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + + # when + options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0) + session.update_link(node1.id, node2.id, options=options) + + # then + assert iface1.local_options.is_clear() + assert not iface1.has_local_netem + assert iface1.options.is_clear() + assert not iface1.has_netem def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given From d5b05a39e821171424c6c85c7164cc47e3030880 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Jan 2022 11:19:30 -0800 Subject: [PATCH 03/21] gui: adjustment to update drawing asymmetric edge data when joining a session --- daemon/core/gui/graph/manager.py | 1 + daemon/core/nodes/base.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/graph/manager.py b/daemon/core/gui/graph/manager.py index 8acce4c9d..dc0adca93 100644 --- a/daemon/core/gui/graph/manager.py +++ b/daemon/core/gui/graph/manager.py @@ -395,6 +395,7 @@ def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: if token in self.edges and link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link + edge.redraw() elif token not in self.edges: edge = CanvasEdge(self.app, src, dst) edge.complete(dst, link) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 03979d012..2cf7d555c 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1055,7 +1055,7 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: unidirectional = 0 linked_node = iface.node if linked_node is None: - # two layer-2 switches/hubs linked together via linknet() + # two layer-2 switches/hubs linked together if not iface.othernet: continue linked_node = iface.othernet @@ -1063,7 +1063,9 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: continue if iface.local_options != iface.options: unidirectional = 1 - iface_data = iface.get_data() + iface_data = None + else: + iface_data = iface.get_data() link_data = LinkData( message_type=flags, type=self.linktype, From 8f767208e089394fea0a96657c30bd7e99c3aa10 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Jan 2022 11:35:23 -0800 Subject: [PATCH 04/21] daemon: small adjustment to fix xml related issues parsing links for now, until bigger refactoring --- daemon/core/nodes/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 2cf7d555c..3b5cd04e2 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1063,9 +1063,7 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: continue if iface.local_options != iface.options: unidirectional = 1 - iface_data = None - else: - iface_data = iface.get_data() + iface_data = iface.get_data() link_data = LinkData( message_type=flags, type=self.linktype, From df5ff02f95b3912598ba12edf982a37191996379 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 1 Feb 2022 21:26:03 -0800 Subject: [PATCH 05/21] docs: fix for bad config service example --- docs/configservices.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/configservices.md b/docs/configservices.md index 42cf14787..4ff8a87c1 100644 --- a/docs/configservices.md +++ b/docs/configservices.md @@ -168,13 +168,12 @@ class ExampleService(ConfigService): ] def get_text_template(self, name: str) -> str: - if name == "example-start.sh": - return """ - # sample script 1 - # node id(${node.id}) name(${node.name}) - # config: ${config} - echo hello - """ + return """ + # sample script 1 + # node id(${node.id}) name(${node.name}) + # config: ${config} + echo hello + """ ``` #### Validation Mode From 44b7b6a27e280c3ff1c8ba5316150f9597314b8e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 3 Feb 2022 14:28:56 -0800 Subject: [PATCH 06/21] daemon: update config service files to use paths for retrieving templates and generating files in the same consistent way --- daemon/core/configservice/base.py | 24 ++++++++++++++----- .../templates/{ => usr/local/etc/frr}/daemons | 0 .../{ => usr/local/etc/frr}/frr.conf | 0 .../{ => usr/local/etc/frr}/vtysh.conf | 0 .../templates/{ => etc/olsrd}/olsrd.conf | 0 .../{ => usr/local/etc/quagga}/Quagga.conf | 0 .../{ => usr/local/etc/quagga}/vtysh.conf | 0 .../templates/{ => etc/apache2}/apache2.conf | 0 .../templates/{ => etc/apache2}/envvars | 0 .../templates/{ => etc/dhcp}/dhcpd.conf | 0 .../templates/{ => etc/radvd}/radvd.conf | 0 .../templates/{ => etc/ssh}/sshd_config | 0 .../templates/{ => var/www}/index.html | 0 13 files changed, 18 insertions(+), 6 deletions(-) rename daemon/core/configservices/frrservices/templates/{ => usr/local/etc/frr}/daemons (100%) rename daemon/core/configservices/frrservices/templates/{ => usr/local/etc/frr}/frr.conf (100%) rename daemon/core/configservices/frrservices/templates/{ => usr/local/etc/frr}/vtysh.conf (100%) rename daemon/core/configservices/nrlservices/templates/{ => etc/olsrd}/olsrd.conf (100%) rename daemon/core/configservices/quaggaservices/templates/{ => usr/local/etc/quagga}/Quagga.conf (100%) rename daemon/core/configservices/quaggaservices/templates/{ => usr/local/etc/quagga}/vtysh.conf (100%) rename daemon/core/configservices/utilservices/templates/{ => etc/apache2}/apache2.conf (100%) rename daemon/core/configservices/utilservices/templates/{ => etc/apache2}/envvars (100%) rename daemon/core/configservices/utilservices/templates/{ => etc/dhcp}/dhcpd.conf (100%) rename daemon/core/configservices/utilservices/templates/{ => etc/radvd}/radvd.conf (100%) rename daemon/core/configservices/utilservices/templates/{ => etc/ssh}/sshd_config (100%) rename daemon/core/configservices/utilservices/templates/{ => var/www}/index.html (100%) diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index 386ab26d8..6e1dc8598 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -19,6 +19,20 @@ TEMPLATES_DIR: str = "templates" +def get_template_path(file_path: Path) -> str: + """ + Utility to convert a given file path to a valid template path format. + + :param file_path: file path to convert + :return: template path + """ + if file_path.is_absolute(): + template_path = str(file_path.relative_to("/")) + else: + template_path = str(file_path) + return template_path + + class ConfigServiceMode(enum.Enum): BLOCKING = 0 NON_BLOCKING = 1 @@ -295,10 +309,7 @@ def get_templates(self) -> Dict[str, str]: templates = {} for file in self.files: file_path = Path(file) - if file_path.is_absolute(): - template_path = str(file_path.relative_to("/")) - else: - template_path = str(file_path) + template_path = get_template_path(file_path) if file in self.custom_templates: template = self.custom_templates[file] template = self.clean_text(template) @@ -322,11 +333,12 @@ def create_files(self) -> None: "node(%s) service(%s) template(%s)", self.node.name, self.name, file ) file_path = Path(file) + template_path = get_template_path(file_path) if file in self.custom_templates: text = self.custom_templates[file] rendered = self.render_text(text, data) - elif self.templates.has_template(file_path.name): - rendered = self.render_template(file_path.name, data) + elif self.templates.has_template(template_path): + rendered = self.render_template(template_path, data) else: text = self.get_text_template(file) rendered = self.render_text(text, data) diff --git a/daemon/core/configservices/frrservices/templates/daemons b/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/daemons similarity index 100% rename from daemon/core/configservices/frrservices/templates/daemons rename to daemon/core/configservices/frrservices/templates/usr/local/etc/frr/daemons diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/frr.conf similarity index 100% rename from daemon/core/configservices/frrservices/templates/frr.conf rename to daemon/core/configservices/frrservices/templates/usr/local/etc/frr/frr.conf diff --git a/daemon/core/configservices/frrservices/templates/vtysh.conf b/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/vtysh.conf similarity index 100% rename from daemon/core/configservices/frrservices/templates/vtysh.conf rename to daemon/core/configservices/frrservices/templates/usr/local/etc/frr/vtysh.conf diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.conf b/daemon/core/configservices/nrlservices/templates/etc/olsrd/olsrd.conf similarity index 100% rename from daemon/core/configservices/nrlservices/templates/olsrd.conf rename to daemon/core/configservices/nrlservices/templates/etc/olsrd/olsrd.conf diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf similarity index 100% rename from daemon/core/configservices/quaggaservices/templates/Quagga.conf rename to daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf diff --git a/daemon/core/configservices/quaggaservices/templates/vtysh.conf b/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf similarity index 100% rename from daemon/core/configservices/quaggaservices/templates/vtysh.conf rename to daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf diff --git a/daemon/core/configservices/utilservices/templates/apache2.conf b/daemon/core/configservices/utilservices/templates/etc/apache2/apache2.conf similarity index 100% rename from daemon/core/configservices/utilservices/templates/apache2.conf rename to daemon/core/configservices/utilservices/templates/etc/apache2/apache2.conf diff --git a/daemon/core/configservices/utilservices/templates/envvars b/daemon/core/configservices/utilservices/templates/etc/apache2/envvars similarity index 100% rename from daemon/core/configservices/utilservices/templates/envvars rename to daemon/core/configservices/utilservices/templates/etc/apache2/envvars diff --git a/daemon/core/configservices/utilservices/templates/dhcpd.conf b/daemon/core/configservices/utilservices/templates/etc/dhcp/dhcpd.conf similarity index 100% rename from daemon/core/configservices/utilservices/templates/dhcpd.conf rename to daemon/core/configservices/utilservices/templates/etc/dhcp/dhcpd.conf diff --git a/daemon/core/configservices/utilservices/templates/radvd.conf b/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf similarity index 100% rename from daemon/core/configservices/utilservices/templates/radvd.conf rename to daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf diff --git a/daemon/core/configservices/utilservices/templates/sshd_config b/daemon/core/configservices/utilservices/templates/etc/ssh/sshd_config similarity index 100% rename from daemon/core/configservices/utilservices/templates/sshd_config rename to daemon/core/configservices/utilservices/templates/etc/ssh/sshd_config diff --git a/daemon/core/configservices/utilservices/templates/index.html b/daemon/core/configservices/utilservices/templates/var/www/index.html similarity index 100% rename from daemon/core/configservices/utilservices/templates/index.html rename to daemon/core/configservices/utilservices/templates/var/www/index.html From 43737a42e4190de0cd7cae4bf7a5621f7286d8e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 3 Feb 2022 14:29:42 -0800 Subject: [PATCH 07/21] daemon: update nftables bridge tables to use priority -1 to beat default inet table rules if present --- daemon/core/nodes/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 32d420dd2..1a964fc00 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -194,7 +194,7 @@ def build_cmds(self, net: "CoreNetwork") -> None: self.cmds.append(f"add table bridge {net.brname}") self.cmds.append( f"add chain bridge {net.brname} {self.chain} {{type filter hook " - f"forward priority 0\\; policy {policy}\\;}}" + f"forward priority -1\\; policy {policy}\\;}}" ) # add default rule to accept all traffic not for this bridge self.cmds.append( From efb97d1a5d3ec02a0ff3b0357c544443b4c2f9b4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Feb 2022 00:39:11 -0800 Subject: [PATCH 08/21] daemon: updates to remove the delay for processing wlan changes along with code to support it --- daemon/core/location/mobility.py | 19 ++++------- daemon/core/nodes/network.py | 56 ++++++++++---------------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 0464ebc7b..ebcb8fe49 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -343,14 +343,12 @@ def set_position(self, iface: CoreInterface) -> None: :return: nothing """ x, y, z = iface.node.position.get() - self.iface_lock.acquire() - self.iface_to_pos[iface] = (x, y, z) - if x is None or y is None: - self.iface_lock.release() - return - for iface2 in self.iface_to_pos: - self.calclink(iface, iface2) - self.iface_lock.release() + with self.iface_lock: + self.iface_to_pos[iface] = (x, y, z) + if x is None or y is None: + return + for iface2 in self.iface_to_pos: + self.calclink(iface, iface2) position_callback = set_position @@ -388,20 +386,15 @@ def calclink(self, iface: CoreInterface, iface2: CoreInterface) -> None: """ if iface == iface2: return - try: x, y, z = self.iface_to_pos[iface] x2, y2, z2 = self.iface_to_pos[iface2] - if x2 is None or y2 is None: return - d = self.calcdistance((x, y, z), (x2, y2, z2)) - # ordering is important, to keep the wlan._linked dict organized a = min(iface, iface2) b = max(iface, iface2) - with self.wlan.linked_lock: linked = self.wlan.is_linked(a, b) if d > self.range: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 1a964fc00..6c5da0890 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -4,7 +4,6 @@ import logging import threading -import time from collections import OrderedDict from pathlib import Path from queue import Queue @@ -80,50 +79,32 @@ def __init__(self) -> None: self.cmds: List[str] = [] # list of WLANs requiring update self.updates: SetQueue = SetQueue() - # timestamps of last WLAN update; this keeps track of WLANs that are - # using this queue - self.last_update_time: Dict["CoreNetwork", float] = {} - def start(self, net: "CoreNetwork") -> None: + def start(self) -> None: """ Start thread to listen for updates for the provided network. - :param net: network to start checking updates + :return: nothing """ with self.lock: - self.last_update_time[net] = time.monotonic() - if self.running: - return - self.running = True - self.run_thread = threading.Thread(target=self.run, daemon=True) - self.run_thread.start() + if not self.running: + self.running = True + self.run_thread = threading.Thread(target=self.run, daemon=True) + self.run_thread.start() - def stop(self, net: "CoreNetwork") -> None: + def stop(self) -> None: """ Stop updates for network, when no networks remain, stop update thread. - :param net: network to stop watching updates + :return: nothing """ with self.lock: - self.last_update_time.pop(net, None) - if self.last_update_time: - return - self.running = False - if self.run_thread: + if self.running: + self.running = False self.updates.put(None) self.run_thread.join() self.run_thread = None - def last_update(self, net: "CoreNetwork") -> float: - """ - Return the time elapsed since this network was last updated. - :param net: network node - :return: elapsed time - """ - now = time.monotonic() - last_update = self.last_update_time.setdefault(net, now) - return now - last_update - def run(self) -> None: """ Thread target that looks for networks needing update, and @@ -136,17 +117,14 @@ def run(self) -> None: net = self.updates.get() if net is None: break - if not net.up: - self.last_update_time[net] = time.monotonic() - elif self.last_update(net) > self.rate: - with self.lock: - self.build_cmds(net) - self.commit(net) - self.last_update_time[net] = time.monotonic() + with self.lock: + self.build_cmds(net) + self.commit(net) def commit(self, net: "CoreNetwork") -> None: """ Commit changes to nftables for the provided network. + :param net: network to commit nftables changes :return: nothing """ @@ -164,6 +142,7 @@ def commit(self, net: "CoreNetwork") -> None: def update(self, net: "CoreNetwork") -> None: """ Flag this network has an update, so the nftables chain will be rebuilt. + :param net: wlan network :return: nothing """ @@ -182,6 +161,7 @@ def delete_table(self, net: "CoreNetwork") -> None: def build_cmds(self, net: "CoreNetwork") -> None: """ Inspect linked nodes for a network, and rebuild the nftables chain commands. + :param net: network to build commands for :return: nothing """ @@ -299,7 +279,7 @@ def startup(self) -> None: self.net_client.set_mtu(self.brname, self.mtu) self.has_nftables_chain = False self.up = True - nft_queue.start(self) + nft_queue.start() def shutdown(self) -> None: """ @@ -309,7 +289,7 @@ def shutdown(self) -> None: """ if not self.up: return - nft_queue.stop(self) + nft_queue.stop() try: self.net_client.delete_bridge(self.brname) if self.has_nftables_chain: From 490a4acf24ad39fbbc517417200d85322d5953b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Feb 2022 00:46:25 -0800 Subject: [PATCH 09/21] daemon: fixed issues related to rj45 --- daemon/core/nodes/physical.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 4dd5fb571..5c1cfe2eb 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -233,7 +233,7 @@ def __init__( """ super().__init__(session, _id, name, server) self.iface: CoreInterface = CoreInterface( - session, self, name, name, mtu, server + session, name, name, mtu, server, self ) self.iface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() @@ -438,3 +438,12 @@ def nodefile(self, file_path: str, contents: str, mode: int = 0o644) -> None: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: raise CoreError("rj45 does not support cmds") + + def create_dir(self, dir_path: Path) -> None: + raise CoreError("rj45 does not support creating directories") + + def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: + raise CoreError("rj45 does not support creating files") + + def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: + raise CoreError("rj45 does not support copying files") From 0e2219f6c8c7c7fe24a35f21dd4e953f6c8ac0c4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Feb 2022 12:19:24 -0800 Subject: [PATCH 10/21] daemon: fixed issue creating directory for files when needed, within node --- daemon/core/nodes/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 6dca41e1e..d5e928de2 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -201,7 +201,7 @@ def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None temp.write(contents.encode("utf-8")) temp.close() temp_path = Path(temp.name) - directory = file_path.name + directory = file_path.parent if str(directory) != ".": self.cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: From 47b02c3e7b12f911b6f89668fcbb16d701463f48 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 9 Feb 2022 12:47:49 -0800 Subject: [PATCH 11/21] docs: removed notes on needing to install ebtables legacy --- docs/install.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/install.md b/docs/install.md index 31a3a0e63..f03b274ee 100644 --- a/docs/install.md +++ b/docs/install.md @@ -24,16 +24,6 @@ Verified: * Ubuntu - 18.04, 20.04 * CentOS - 7.8, 8.0 -> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN functionality - -Enabling ebtables legacy: -```shell -sudo apt install ebtables -update-alternatives --set ebtables /usr/sbin/ebtables-legacy -``` - -> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not function properly - > **NOTE:** CentOS 8 does not have the netem kernel mod available by default CentOS 8 Enabled netem: From a1e9fc02fdb760ec658f37c823451bc5c8d97938 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:11:43 -0800 Subject: [PATCH 12/21] install: bumped pinned poetry version to 1.1.12 to avoid installation issues related to poetry --- setup.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.sh b/setup.sh index 3215cdb7f..eb81a4518 100755 --- a/setup.sh +++ b/setup.sh @@ -1,8 +1,5 @@ #!/bin/bash -# exit on error -set -e - # install pre-reqs using yum/apt if command -v apt &> /dev/null then @@ -23,4 +20,4 @@ python3 -m pip install --user pipx==0.16.4 python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke==1.4.1 -pipx install poetry==1.1.7 +pipx install poetry==1.1.12 From e80d22096c494f288ee2768930c8e9818cc8acd3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:17:22 -0800 Subject: [PATCH 13/21] install: bumping version to 8.1.0 for next release --- configure.ac | 2 +- daemon/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 6b06966ab..a3d61abc5 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, 8.0.0) +AC_INIT(core, 8.1.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 18a4464e7..f1ab7dd48 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "8.0.0" +version = "8.1.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" From 0fcc532c0de9e79f96fcdca522a27da1747f9cb9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Feb 2022 21:04:12 -0800 Subject: [PATCH 14/21] docs: updated note on poetry version installed --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index f03b274ee..8874984af 100644 --- a/docs/install.md +++ b/docs/install.md @@ -130,7 +130,7 @@ First you can use `setup.sh` as a convenience to install tooling for running inv * python3, pip, venv * pipx 0.16.4 via pip * invoke 1.4.1 via pipx -* poetry 1.1.7 via pipx +* poetry 1.1.12 via pipx Then you can run `inv install `: * installs system dependencies for building core From 3c646545987118cc442c51e20c474a0382567418 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Feb 2022 10:14:04 -0800 Subject: [PATCH 15/21] services: fix missing configurations for ospfv2 in config services --- .../configservices/quaggaservices/services.py | 50 ++++++++++++++++--- .../usr/local/etc/quagga/Quagga.conf | 10 ++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index fba892b42..a4ee157d7 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -7,7 +7,8 @@ from core.emane.nodes import EmaneNet from core.nodes.base import CoreNodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface -from core.nodes.network import WlanNode +from core.nodes.network import PtpNet, WlanNode +from core.nodes.physical import Rj45Node logger = logging.getLogger(__name__) GROUP: str = "Quagga" @@ -55,6 +56,20 @@ def get_router_id(node: CoreNodeBase) -> str: return "0.0.0.0" +def rj45_check(iface: CoreInterface) -> bool: + """ + Helper to detect whether interface is connected an external RJ45 + link. + """ + if iface.net: + for peer_iface in iface.net.get_ifaces(): + if peer_iface == iface: + continue + if isinstance(peer_iface.node, Rj45Node): + return True + return False + + class Zebra(ConfigService): name: str = "zebra" group: str = GROUP @@ -105,7 +120,13 @@ def data(self) -> Dict[str, Any]: ip4s.append(str(ip4)) for ip6 in iface.ip6s: ip6s.append(str(ip6)) - ifaces.append((iface, ip4s, ip6s, iface.control)) + configs = [] + if not iface.control: + for service in services: + config = service.quagga_iface_config(iface) + if config: + configs.append(config.split("\n")) + ifaces.append((iface, ip4s, ip6s, configs)) return dict( quagga_bin_search=quagga_bin_search, @@ -156,17 +177,32 @@ class Ospfv2(QuaggaService, ConfigService): ipv4_routing: bool = True def quagga_iface_config(self, iface: CoreInterface) -> str: - if has_mtu_mismatch(iface): - return "ip ospf mtu-ignore" - else: - return "" + has_mtu = has_mtu_mismatch(iface) + has_rj45 = rj45_check(iface) + is_ptp = isinstance(iface.net, PtpNet) + data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45) + text = """ + % if has_mtu: + ip ospf mtu-ignore + % endif + % if has_rj45: + <% return STOP_RENDERING %> + % endif + % if is_ptp: + ip ospf network point-to-point + % endif + ip ospf hello-interval 2 + ip ospf dead-interval 6 + ip ospf retransmit-interval 5 + """ + return self.render_text(text, data) def quagga_config(self) -> str: router_id = get_router_id(self.node) addresses = [] for iface in self.node.get_ifaces(control=False): for ip4 in iface.ip4s: - addresses.append(str(ip4.ip)) + addresses.append(str(ip4)) data = dict(router_id=router_id, addresses=addresses) text = """ router ospf diff --git a/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf index 1d69838f0..b7916f965 100644 --- a/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf +++ b/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf @@ -1,4 +1,4 @@ -% for iface, ip4s, ip6s, is_control in ifaces: +% for iface, ip4s, ip6s, configs in ifaces: interface ${iface.name} % if want_ip4: % for addr in ip4s: @@ -10,13 +10,11 @@ interface ${iface.name} ipv6 address ${addr} % endfor % endif - % if not is_control: - % for service in services: - % for line in service.quagga_iface_config(iface).split("\n"): + % for config in configs: + % for line in config: ${line} - % endfor % endfor - % endif + % endfor ! % endfor From 9d32c432dbc2ab29fbf43f58a7a3af32c4c15ce8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Feb 2022 15:03:09 -0800 Subject: [PATCH 16/21] install: updates to README and install docs for installing to run ensurepath after setup.sh ends --- README.md | 3 +++ docs/install.md | 8 ++++++++ setup.sh | 1 - 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc991f14c..314d231e8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ For more detailed installation see [here](https://coreemu.github.io/core/install ```shell git clone https://github.com/coreemu/core.git cd core +# install dependencies to run installation task ./setup.sh +# run the following or add ~/.local/bin to PATH +python3 -m pipx ensurepath # Ubuntu inv install # CentOS diff --git a/docs/install.md b/docs/install.md index 8874984af..dc9903a5b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -120,6 +120,14 @@ First we will need to clone and navigate to the CORE repo. # clone CORE repo git clone https://github.com/coreemu/core.git cd core +# install dependencies to run installation task +./setup.sh +# run the following or add ~/.local/bin to PATH +python3 -m pipx ensurepath +# Ubuntu +inv install +# CentOS +./install.sh -p /usr ``` First you can use `setup.sh` as a convenience to install tooling for running invoke tasks: diff --git a/setup.sh b/setup.sh index eb81a4518..13c25de9e 100755 --- a/setup.sh +++ b/setup.sh @@ -17,7 +17,6 @@ fi # install tooling for invoke based installation python3 -m pip install --user pipx==0.16.4 -python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke==1.4.1 pipx install poetry==1.1.12 From 113be650784da572e65c6f41bb2a169c055ce824 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Feb 2022 15:17:07 -0800 Subject: [PATCH 17/21] install: adjustments to have a better workflow for installation --- README.md | 4 ++-- docs/install.md | 4 ++-- setup.sh | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 314d231e8..3cd4ae9ee 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ git clone https://github.com/coreemu/core.git cd core # install dependencies to run installation task ./setup.sh -# run the following or add ~/.local/bin to PATH -python3 -m pipx ensurepath +# run the following or open a new terminal +source ~/.bashrc # Ubuntu inv install # CentOS diff --git a/docs/install.md b/docs/install.md index dc9903a5b..2ba72fc5b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -122,8 +122,8 @@ git clone https://github.com/coreemu/core.git cd core # install dependencies to run installation task ./setup.sh -# run the following or add ~/.local/bin to PATH -python3 -m pipx ensurepath +# run the following or open a new terminal +source ~/.bashrc # Ubuntu inv install # CentOS diff --git a/setup.sh b/setup.sh index 13c25de9e..eb81a4518 100755 --- a/setup.sh +++ b/setup.sh @@ -17,6 +17,7 @@ fi # install tooling for invoke based installation python3 -m pip install --user pipx==0.16.4 +python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke==1.4.1 pipx install poetry==1.1.12 From 96f2408e012e83e757400b953c1d806219c9f4b2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Feb 2022 23:44:30 -0800 Subject: [PATCH 18/21] daemon: fixed deadlock issue when starting/stopping nftables queue --- daemon/core/nodes/network.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6c5da0890..4337daa88 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -117,9 +117,8 @@ def run(self) -> None: net = self.updates.get() if net is None: break - with self.lock: - self.build_cmds(net) - self.commit(net) + self.build_cmds(net) + self.commit(net) def commit(self, net: "CoreNetwork") -> None: """ From 458b7f15ce32642453e85173d313888213a377f4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Feb 2022 00:24:30 -0800 Subject: [PATCH 19/21] pygui: fixed antenna image to properly show alpha png --- daemon/core/gui/data/icons/antenna.gif | Bin 230 -> 0 bytes daemon/core/gui/data/icons/antenna.png | Bin 0 -> 385 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 daemon/core/gui/data/icons/antenna.gif create mode 100644 daemon/core/gui/data/icons/antenna.png diff --git a/daemon/core/gui/data/icons/antenna.gif b/daemon/core/gui/data/icons/antenna.gif deleted file mode 100644 index 55814324be26242f061339e6cd85d0ebfa67c8b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230 zcmZ?wbhEHb)Me0RIK;{T1Zin$?H&D_H*bFS?Ad=9Q2fWIk*sNCU|^=;l%JZJm(HO0 zlZBCsft^7Iqz9y%fq7=dt~>t>PI<20Yw>z*%LIA$wu$OaDpThjU$rSV+3545te3L4 z4c2bo87$x;EGEftbE0tw!@SeeF1l5zS^GY_bn3O=?wHJw67KH-e4D?=Sg*L9zEOBr z-}0AbT~_ipKg-wE^ENhdx3uO}bToH$b2Cop7bHOy`C3Coz bFfU_Yv1-jSmJO>mux#46e%q>aP6lfLpq*ob diff --git a/daemon/core/gui/data/icons/antenna.png b/daemon/core/gui/data/icons/antenna.png new file mode 100644 index 0000000000000000000000000000000000000000..4247aa3d8249ee278add7d01cd201c75d9722989 GIT binary patch literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^x**KK1|+Sd9?b$$Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPllMsivFta(=ZlI7|D9W51*bK8ci&yS?Q}_IB`fdkGHo88{?w>g+ WWb3qvhd12^g@mW8pUXO@geCy0ZkaUz literal 0 HcmV?d00001 From a341691d69c6582105d0085c0f8147e9d2642277 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Feb 2022 00:33:18 -0800 Subject: [PATCH 20/21] install: poetry updates to fix vulnerability issues with protobuf, py, and cryptography --- daemon/poetry.lock | 557 ++++++++++++++++++++++++------------------ daemon/pyproject.toml | 2 +- 2 files changed, 318 insertions(+), 241 deletions(-) diff --git a/daemon/poetry.lock b/daemon/poetry.lock index b29e83690..9055a0b50 100644 --- a/daemon/poetry.lock +++ b/daemon/poetry.lock @@ -16,16 +16,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.1.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +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" @@ -62,7 +63,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "cffi" -version = "1.14.2" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -81,15 +82,19 @@ python-versions = ">=3.6" [[package]] name = "click" -version = "7.1.2" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +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.3" +version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -97,27 +102,26 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cryptography" -version = "3.0" +version = "36.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.6" [package.dependencies] -cffi = ">=1.8,<1.11.3 || >1.11.3" -six = ">=1.4.1" +cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -idna = ["idna (>=2.1)"] +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 = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "dataclasses" -version = "0.7" +version = "0.8" description = "A backport of the dataclasses module for Python 3.6" category = "main" optional = false @@ -125,7 +129,7 @@ python-versions = ">=3.6, <3.7" [[package]] name = "distlib" -version = "0.3.1" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -149,11 +153,15 @@ testing = ["mock (>=2.0.0,<3.0)"] [[package]] name = "filelock" -version = "3.0.12" +version = "3.4.1" description = "A platform independent file lock." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" + +[package.extras] +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" @@ -194,7 +202,7 @@ protobuf = ">=3.5.0.post1" [[package]] name = "identify" -version = "1.4.28" +version = "1.6.2" description = "File identification library for Python" category = "dev" optional = false @@ -205,32 +213,35 @@ license = ["editdistance"] [[package]] name = "importlib-metadata" -version = "1.7.0" +version = "4.8.3" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.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", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +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 = "3.0.0" +version = "5.4.0" description = "Read resources from Python packages" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["sphinx", "rst.linker", "jaraco.packaging"] +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" @@ -285,11 +296,11 @@ lingua = ["lingua"] [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "mccabe" @@ -314,7 +325,7 @@ test = ["pytest", "pytest-cov"] [[package]] name = "more-itertools" -version = "8.4.0" +version = "8.12.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -330,7 +341,7 @@ python-versions = "*" [[package]] name = "nodeenv" -version = "1.4.0" +version = "1.6.0" description = "Node.js virtual environment builder" category = "dev" optional = false @@ -338,19 +349,18 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.4" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" -six = "*" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.7.1" +version = "2.9.2" description = "SSH2 protocol library" category = "main" optional = false @@ -375,6 +385,18 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "platformdirs" +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" @@ -409,22 +431,19 @@ virtualenv = ">=15.2" [[package]] name = "protobuf" -version = "3.12.2" +version = "3.19.4" description = "Protocol Buffers" category = "main" optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.9" +python-versions = ">=3.5" [[package]] name = "py" -version = "1.9.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" @@ -436,7 +455,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -452,15 +471,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pynacl" -version = "1.4.0" +version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.4.1" -six = "*" [package.extras] docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] @@ -468,11 +486,14 @@ tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyproj" @@ -515,7 +536,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false @@ -523,31 +544,39 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" [[package]] name = "virtualenv" -version = "20.0.31" +version = "20.13.1" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -appdirs = ">=1.4.3,<2" distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" -importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +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 = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=5)", "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)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +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" @@ -559,20 +588,20 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.1.0" +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 (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +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.6" -content-hash = "ce58fdcb0bde84e407f05bb89472405dd231cdec02ba84cabd47510bee8b3cc2" +content-hash = "64ea28583e46b32b3aa2be3627ee8f68c1bbf36622ec6f575062d5059745a6f9" [metadata.files] appdirs = [ @@ -584,8 +613,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, - {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, + {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-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, @@ -601,83 +630,106 @@ black = [ {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, ] cffi = [ - {file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"}, - {file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"}, - {file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"}, - {file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"}, - {file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"}, - {file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"}, - {file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"}, - {file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"}, - {file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"}, - {file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"}, - {file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"}, - {file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"}, - {file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"}, - {file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"}, - {file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"}, - {file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"}, + {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.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, + {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-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, - {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, - {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, - {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, - {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, - {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, - {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, - {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, - {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, - {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, - {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, - {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, - {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, - {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, - {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, + {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, + {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, + {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, + {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, + {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, + {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, + {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, + {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, ] dataclasses = [ - {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, - {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, + {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.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, + {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.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, ] filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, + {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"}, @@ -774,16 +826,16 @@ grpcio-tools = [ {file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"}, ] identify = [ - {file = "identify-1.4.28-py2.py3-none-any.whl", hash = "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6"}, - {file = "identify-1.4.28.tar.gz", hash = "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"}, + {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"}, + {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, + {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-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, - {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, + {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.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, @@ -861,58 +913,75 @@ mako = [ {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {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"}, @@ -923,24 +992,24 @@ mock = [ {file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"}, ] more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, + {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, + {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] 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.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, - {file = "nodeenv-1.4.0.tar.gz", hash = "sha256:26941644654d8dd5378720e38f62a3bac5f9240811fb3b8913d2663a17baa91c"}, + {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-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.7.1-py2.py3-none-any.whl", hash = "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"}, - {file = "paramiko-2.7.1.tar.gz", hash = "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f"}, + {file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"}, + {file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"}, ] pillow = [ {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, @@ -997,6 +1066,10 @@ pillow = [ {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-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -1006,64 +1079,64 @@ pre-commit = [ {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, ] protobuf = [ - {file = "protobuf-3.12.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c"}, - {file = "protobuf-3.12.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776"}, - {file = "protobuf-3.12.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a"}, - {file = "protobuf-3.12.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3"}, - {file = "protobuf-3.12.2-cp35-cp35m-win32.whl", hash = "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07"}, - {file = "protobuf-3.12.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925"}, - {file = "protobuf-3.12.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea"}, - {file = "protobuf-3.12.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e"}, - {file = "protobuf-3.12.2-cp36-cp36m-win32.whl", hash = "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122"}, - {file = "protobuf-3.12.2-cp36-cp36m-win_amd64.whl", hash = "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f"}, - {file = "protobuf-3.12.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0"}, - {file = "protobuf-3.12.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907"}, - {file = "protobuf-3.12.2-cp37-cp37m-win32.whl", hash = "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2"}, - {file = "protobuf-3.12.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e"}, - {file = "protobuf-3.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828"}, - {file = "protobuf-3.12.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9"}, - {file = "protobuf-3.12.2-py2.py3-none-any.whl", hash = "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb"}, - {file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"}, + {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.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] 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 = [ - {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, - {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, - {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, - {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, - {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, + {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"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {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-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {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-2.6.1.post1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:457ad3856014ac26af1d86def6dc8cf69c1fa377b6e2fd6e97912d51cf66bdbe"}, @@ -1118,22 +1191,26 @@ pyyaml = [ {file = "PyYAML-5.4.tar.gz", hash = "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {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.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, - {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, + {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, + {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, ] 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.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + {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/pyproject.toml b/daemon/pyproject.toml index f1ab7dd48..a92cc994a 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -25,7 +25,7 @@ lxml = "4.6.5" mako = "1.1.3" netaddr = "0.7.19" pillow = "8.3.2" -protobuf = "3.12.2" +protobuf = "3.19.4" pyproj = "2.6.1.post1" pyyaml = "5.4" From cdde1c89ee48032574372574f0e41782af7a4cae Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Feb 2022 09:08:22 -0800 Subject: [PATCH 21/21] added 8.1.0 notes to changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05e52e007..ce95c5fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 2022-02-18 CORE 8.1.0 + +* Installation + * updated dependency versions to account for known vulnerabilities +* GUI + * fixed issue drawing asymmetric link configurations when joining a session +* daemon + * fixed issue getting templates and creating files for config services + * added by directional support for network to network links + * \#647 - fixed issue when creating RJ45 nodes + * \#646 - fixed issue when creating files for Docker nodes + * \#645 - improved wlan change updates to account for all updates with no delay +* services + * fixed file generation for OSPFv2 config service + ## 2022-01-12 CORE 8.0.0 *Breaking Changes