Merge pull request #654 from coreemu/develop

8.1.0 merge
This commit is contained in:
bharnden 2022-02-18 09:12:33 -08:00 committed by GitHub
commit bcf7429785
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 931 additions and 766 deletions

View file

@ -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 ## 2022-01-12 CORE 8.0.0
*Breaking Changes *Breaking Changes

View file

@ -25,7 +25,10 @@ For more detailed installation see [here](https://coreemu.github.io/core/install
```shell ```shell
git clone https://github.com/coreemu/core.git git clone https://github.com/coreemu/core.git
cd core cd core
# install dependencies to run installation task
./setup.sh ./setup.sh
# run the following or open a new terminal
source ~/.bashrc
# Ubuntu # Ubuntu
inv install inv install
# CentOS # CentOS

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # 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 # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -288,17 +288,25 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
# create links # create links
links = [] links = []
asym_links = [] edit_links = []
known_links = set()
for link in request.session.links: for link in request.session.links:
if link.options.unidirectional: iface1 = link.iface1.id if link.iface1 else None
asym_links.append(link) 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: 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) links.append(link)
_, exceptions = grpcutils.create_links(session, links) _, exceptions = grpcutils.create_links(session, links)
if exceptions: if exceptions:
exceptions = [str(x) for x in exceptions] exceptions = [str(x) for x in exceptions]
return core_pb2.StartSessionResponse(result=False, exceptions=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: if exceptions:
exceptions = [str(x) for x in exceptions] exceptions = [str(x) for x in exceptions]
return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) return core_pb2.StartSessionResponse(result=False, exceptions=exceptions)

View file

@ -19,6 +19,20 @@ logger = logging.getLogger(__name__)
TEMPLATES_DIR: str = "templates" 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): class ConfigServiceMode(enum.Enum):
BLOCKING = 0 BLOCKING = 0
NON_BLOCKING = 1 NON_BLOCKING = 1
@ -295,10 +309,7 @@ class ConfigService(abc.ABC):
templates = {} templates = {}
for file in self.files: for file in self.files:
file_path = Path(file) file_path = Path(file)
if file_path.is_absolute(): template_path = get_template_path(file_path)
template_path = str(file_path.relative_to("/"))
else:
template_path = str(file_path)
if file in self.custom_templates: if file in self.custom_templates:
template = self.custom_templates[file] template = self.custom_templates[file]
template = self.clean_text(template) template = self.clean_text(template)
@ -322,11 +333,12 @@ class ConfigService(abc.ABC):
"node(%s) service(%s) template(%s)", self.node.name, self.name, file "node(%s) service(%s) template(%s)", self.node.name, self.name, file
) )
file_path = Path(file) file_path = Path(file)
template_path = get_template_path(file_path)
if file in self.custom_templates: if file in self.custom_templates:
text = self.custom_templates[file] text = self.custom_templates[file]
rendered = self.render_text(text, data) rendered = self.render_text(text, data)
elif self.templates.has_template(file_path.name): elif self.templates.has_template(template_path):
rendered = self.render_template(file_path.name, data) rendered = self.render_template(template_path, data)
else: else:
text = self.get_text_template(file) text = self.get_text_template(file)
rendered = self.render_text(text, data) rendered = self.render_text(text, data)

View file

@ -7,7 +7,8 @@ from core.configservice.base import ConfigService, ConfigServiceMode
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNodeBase from core.nodes.base import CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface 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__) logger = logging.getLogger(__name__)
GROUP: str = "Quagga" GROUP: str = "Quagga"
@ -55,6 +56,20 @@ def get_router_id(node: CoreNodeBase) -> str:
return "0.0.0.0" 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): class Zebra(ConfigService):
name: str = "zebra" name: str = "zebra"
group: str = GROUP group: str = GROUP
@ -105,7 +120,13 @@ class Zebra(ConfigService):
ip4s.append(str(ip4)) ip4s.append(str(ip4))
for ip6 in iface.ip6s: for ip6 in iface.ip6s:
ip6s.append(str(ip6)) 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( return dict(
quagga_bin_search=quagga_bin_search, quagga_bin_search=quagga_bin_search,
@ -156,17 +177,32 @@ class Ospfv2(QuaggaService, ConfigService):
ipv4_routing: bool = True ipv4_routing: bool = True
def quagga_iface_config(self, iface: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(iface): has_mtu = has_mtu_mismatch(iface)
return "ip ospf mtu-ignore" has_rj45 = rj45_check(iface)
else: is_ptp = isinstance(iface.net, PtpNet)
return "" 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: def quagga_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
addresses = [] addresses = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
for ip4 in iface.ip4s: for ip4 in iface.ip4s:
addresses.append(str(ip4.ip)) addresses.append(str(ip4))
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf

View file

@ -1,4 +1,4 @@
% for iface, ip4s, ip6s, is_control in ifaces: % for iface, ip4s, ip6s, configs in ifaces:
interface ${iface.name} interface ${iface.name}
% if want_ip4: % if want_ip4:
% for addr in ip4s: % for addr in ip4s:
@ -10,13 +10,11 @@ interface ${iface.name}
ipv6 address ${addr} ipv6 address ${addr}
% endfor % endfor
% endif % endif
% if not is_control: % for config in configs:
% for service in services: % for line in config:
% for line in service.quagga_iface_config(iface).split("\n"):
${line} ${line}
% endfor
% endfor % endfor
% endif % endfor
! !
% endfor % endfor

View file

@ -2,7 +2,7 @@
CORE data objects. CORE data objects.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, Any, List, Optional, Tuple
import netaddr import netaddr
@ -176,6 +176,67 @@ class LinkOptions:
key: int = None key: int = None
buffer: 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 @dataclass
class LinkData: class LinkData:

View file

@ -276,20 +276,22 @@ class Session:
ptp = self.create_node(PtpNet, start) ptp = self.create_node(PtpNet, start)
iface1 = node1.new_iface(ptp, iface1_data) iface1 = node1.new_iface(ptp, iface1_data)
iface2 = node2.new_iface(ptp, iface2_data) iface2 = node2.new_iface(ptp, iface2_data)
ptp.linkconfig(iface1, options) iface1.config(options)
if not options.unidirectional: if not options.unidirectional:
ptp.linkconfig(iface2, options) iface2.config(options)
# link node to net # link node to net
elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): 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) iface1 = node1.new_iface(node2, iface1_data)
if not isinstance(node2, (EmaneNet, WlanNode)): if not isinstance(node2, (EmaneNet, WlanNode)):
node2.linkconfig(iface1, options) iface1.config(options)
# link net to node # link net to node
elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): 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) iface2 = node2.new_iface(node1, iface2_data)
wireless_net = isinstance(node1, (EmaneNet, WlanNode)) wireless_net = isinstance(node1, (EmaneNet, WlanNode))
if not options.unidirectional and not wireless_net: if not options.unidirectional and not wireless_net:
node1.linkconfig(iface2, options) iface2.config(options)
# network to network # network to network
elif isinstance(node1, CoreNetworkBase) and isinstance( elif isinstance(node1, CoreNetworkBase) and isinstance(
node2, CoreNetworkBase node2, CoreNetworkBase
@ -298,11 +300,10 @@ class Session:
"linking network to network: %s - %s", node1.name, node2.name "linking network to network: %s - %s", node1.name, node2.name
) )
iface1 = node1.linknet(node2) iface1 = node1.linknet(node2)
node1.linkconfig(iface1, options) use_local = iface1.net == node1
iface1.config(options, use_local=use_local)
if not options.unidirectional: if not options.unidirectional:
iface1.swapparams("_params_up") iface1.config(options, use_local=not use_local)
node2.linkconfig(iface1, options)
iface1.swapparams("_params_up")
else: else:
raise CoreError( raise CoreError(
f"cannot link node1({type(node1)}) node2({type(node2)})" f"cannot link node1({type(node1)}) node2({type(node2)})"
@ -379,16 +380,18 @@ class Session:
elif isinstance(node1, CoreNetworkBase) and isinstance( elif isinstance(node1, CoreNetworkBase) and isinstance(
node2, CoreNetworkBase node2, CoreNetworkBase
): ):
for iface in node1.get_ifaces(control=False): iface1 = node1.get_linked_iface(node2)
if iface.othernet == node2: if iface1:
node1.detach(iface) node1.detach(iface1)
iface.shutdown() iface1.shutdown()
break iface2 = node2.get_linked_iface(node1)
for iface in node2.get_ifaces(control=False): if iface2:
if iface.othernet == node1: node2.detach(iface2)
node2.detach(iface) iface2.shutdown()
iface.shutdown() if not iface1 and not iface2:
break raise CoreError(
f"node1({node1.name}) and node2({node2.name}) are not connected"
)
self.sdt.delete_link(node1_id, node2_id) self.sdt.delete_link(node1_id, node2_id)
def update_link( def update_link(
@ -432,11 +435,11 @@ class Session:
else: else:
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
iface1 = node1.ifaces.get(iface1_id) iface1 = node1.ifaces.get(iface1_id)
iface2 = node2.ifaces.get(iface2_id)
if not iface1: if not iface1:
raise CoreError( raise CoreError(
f"node({node1.name}) missing interface({iface1_id})" f"node({node1.name}) missing interface({iface1_id})"
) )
iface2 = node2.ifaces.get(iface2_id)
if not iface2: if not iface2:
raise CoreError( raise CoreError(
f"node({node2.name}) missing interface({iface2_id})" f"node({node2.name}) missing interface({iface2_id})"
@ -446,39 +449,40 @@ class Session:
f"node1({node1.name}) node2({node2.name}) " f"node1({node1.name}) node2({node2.name}) "
"not connected to same net" "not connected to same net"
) )
ptp = iface1.net iface1.config(options)
ptp.linkconfig(iface1, options, iface2)
if not options.unidirectional: if not options.unidirectional:
ptp.linkconfig(iface2, options, iface1) iface2.config(options)
elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
iface = node1.get_iface(iface1_id) 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): elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
iface = node2.get_iface(iface2_id) 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( elif isinstance(node1, CoreNetworkBase) and isinstance(
node2, CoreNetworkBase node2, CoreNetworkBase
): ):
iface = node1.get_linked_iface(node2) iface = node1.get_linked_iface(node2)
upstream = False
if not iface: if not iface:
upstream = True
iface = node2.get_linked_iface(node1) iface = node2.get_linked_iface(node1)
if not iface: if iface:
raise CoreError("modify unknown link between nets") use_local = iface.net == node1
if upstream: iface.config(options, use_local=use_local)
iface.swapparams("_params_up") if not options.unidirectional:
node1.linkconfig(iface, options) iface.config(options, use_local=not use_local)
iface.swapparams("_params_up")
else: else:
node1.linkconfig(iface, options) raise CoreError(
if not options.unidirectional: f"node1({node1.name}) and node2({node2.name}) are not linked"
if upstream: )
node2.linkconfig(iface, options)
else:
iface.swapparams("_params_up")
node2.linkconfig(iface, options)
iface.swapparams("_params_up")
else: else:
raise CoreError( raise CoreError(
f"cannot update link node1({type(node1)}) node2({type(node2)})" f"cannot update link node1({type(node1)}) node2({type(node2)})"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

View file

@ -395,6 +395,7 @@ class CanvasManager:
if token in self.edges and link.options.unidirectional: if token in self.edges and link.options.unidirectional:
edge = self.edges[token] edge = self.edges[token]
edge.asymmetric_link = link edge.asymmetric_link = link
edge.redraw()
elif token not in self.edges: elif token not in self.edges:
edge = CanvasEdge(self.app, src, dst) edge = CanvasEdge(self.app, src, dst)
edge.complete(dst, link) edge.complete(dst, link)

View file

@ -321,7 +321,7 @@ class BasicRangeModel(WirelessModel):
loss=self.loss, loss=self.loss,
jitter=self.jitter, jitter=self.jitter,
) )
self.wlan.linkconfig(iface, options) iface.config(options)
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
""" """
@ -343,14 +343,12 @@ class BasicRangeModel(WirelessModel):
:return: nothing :return: nothing
""" """
x, y, z = iface.node.position.get() x, y, z = iface.node.position.get()
self.iface_lock.acquire() with self.iface_lock:
self.iface_to_pos[iface] = (x, y, z) self.iface_to_pos[iface] = (x, y, z)
if x is None or y is None: if x is None or y is None:
self.iface_lock.release() return
return for iface2 in self.iface_to_pos:
for iface2 in self.iface_to_pos: self.calclink(iface, iface2)
self.calclink(iface, iface2)
self.iface_lock.release()
position_callback = set_position position_callback = set_position
@ -388,20 +386,15 @@ class BasicRangeModel(WirelessModel):
""" """
if iface == iface2: if iface == iface2:
return return
try: try:
x, y, z = self.iface_to_pos[iface] x, y, z = self.iface_to_pos[iface]
x2, y2, z2 = self.iface_to_pos[iface2] x2, y2, z2 = self.iface_to_pos[iface2]
if x2 is None or y2 is None: if x2 is None or y2 is None:
return return
d = self.calcdistance((x, y, z), (x2, y2, z2)) d = self.calcdistance((x, y, z), (x2, y2, z2))
# ordering is important, to keep the wlan._linked dict organized # ordering is important, to keep the wlan._linked dict organized
a = min(iface, iface2) a = min(iface, iface2)
b = max(iface, iface2) b = max(iface, iface2)
with self.wlan.linked_lock: with self.wlan.linked_lock:
linked = self.wlan.is_linked(a, b) linked = self.wlan.is_linked(a, b)
if d > self.range: if d > self.range:

View file

@ -13,7 +13,7 @@ import netaddr
from core import utils from core import utils
from core.configservice.dependencies import ConfigServiceDependencies 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.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, VNODED from core.executables import MOUNT, TEST, VNODED
@ -1000,20 +1000,6 @@ class CoreNetworkBase(NodeBase):
""" """
raise NotImplementedError 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: def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
raise NotImplementedError raise NotImplementedError
@ -1063,66 +1049,41 @@ class CoreNetworkBase(NodeBase):
:return: list of link data :return: list of link data
""" """
all_links = [] all_links = []
# build a link message from this network node to each node having a # build a link message from this network node to each node having a
# connected interface # connected interface
for iface in self.get_ifaces(): for iface in self.get_ifaces():
uni = False unidirectional = 0
linked_node = iface.node linked_node = iface.node
if linked_node is None: 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: if not iface.othernet:
continue continue
linked_node = iface.othernet linked_node = iface.othernet
if linked_node.id == self.id: if linked_node.id == self.id:
continue continue
iface.swapparams("_params_up") if iface.local_options != iface.options:
upstream_params = iface.getparams() unidirectional = 1
iface.swapparams("_params_up") iface_data = iface.get_data()
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)
link_data = LinkData( link_data = LinkData(
message_type=flags, message_type=flags,
type=self.linktype, type=self.linktype,
node1_id=self.id, node1_id=self.id,
node2_id=linked_node.id, node2_id=linked_node.id,
iface2=iface2_data, iface2=iface_data,
options=options_data, options=iface.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
if unidirectional:
if not uni: link_data = LinkData(
continue message_type=MessageFlags.NONE,
iface.swapparams("_params_up") type=self.linktype,
options_data = iface.get_link_options(unidirectional) node1_id=linked_node.id,
link_data = LinkData( node2_id=self.id,
message_type=MessageFlags.NONE, options=iface.options,
type=self.linktype, )
node1_id=linked_node.id, link_data.options.unidirectional = unidirectional
node2_id=self.id, all_links.append(link_data)
options=options_data,
)
iface.swapparams("_params_up")
all_links.append(link_data)
return all_links return all_links

View file

@ -201,7 +201,7 @@ class DockerNode(CoreNode):
temp.write(contents.encode("utf-8")) temp.write(contents.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name) temp_path = Path(temp.name)
directory = file_path.name directory = file_path.parent
if str(directory) != ".": if str(directory) != ".":
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:

View file

@ -3,16 +3,18 @@ virtual ethernet classes that implement the interfaces available under Linux.
""" """
import logging import logging
import math
import time import time
from pathlib import Path 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 import netaddr
from core import utils 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.emulator.enumerations import TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import TC
from core.nodes.netclient import LinuxNetClient, get_net_client from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,6 +27,50 @@ if TYPE_CHECKING:
DEFAULT_MTU: int = 1500 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: class CoreInterface:
""" """
Base class for network interfaces. Base class for network interfaces.
@ -61,7 +107,6 @@ class CoreInterface:
self.mtu: int = mtu self.mtu: int = mtu
self.net: Optional[CoreNetworkBase] = None self.net: Optional[CoreNetworkBase] = None
self.othernet: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None
self._params: Dict[str, float] = {}
self.ip4s: List[netaddr.IPNetwork] = [] self.ip4s: List[netaddr.IPNetwork] = []
self.ip6s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = []
self.mac: Optional[netaddr.EUI] = None self.mac: Optional[netaddr.EUI] = None
@ -80,6 +125,11 @@ class CoreInterface:
self.session.use_ovs(), self.host_cmd self.session.use_ovs(), self.host_cmd
) )
self.control: bool = False 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( def host_cmd(
self, self,
@ -219,89 +269,6 @@ class CoreInterface:
except netaddr.AddrFormatError as e: except netaddr.AddrFormatError as e:
raise CoreError(f"invalid mac address({mac}): {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: def setposition(self) -> None:
""" """
Dispatch position hook handler when possible. Dispatch position hook handler when possible.
@ -336,6 +303,65 @@ class CoreInterface:
""" """
return self.transport_type == TransportType.VIRTUAL return self.transport_type == TransportType.VIRTUAL
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 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
# 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)
if use_local:
self.has_local_netem = False
else:
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): class Veth(CoreInterface):
""" """

View file

@ -3,9 +3,7 @@ Defines network nodes used within core.
""" """
import logging import logging
import math
import threading import threading
import time
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
@ -14,7 +12,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Type
import netaddr import netaddr
from core import utils from core import utils
from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.data import InterfaceData, LinkData
from core.emulator.enumerations import ( from core.emulator.enumerations import (
LinkTypes, LinkTypes,
MessageFlags, MessageFlags,
@ -23,7 +21,7 @@ from core.emulator.enumerations import (
RegisterTlvs, RegisterTlvs,
) )
from core.errors import CoreCommandError, CoreError 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.base import CoreNetworkBase
from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.interface import CoreInterface, GreTap, Veth
from core.nodes.netclient import get_net_client from core.nodes.netclient import get_net_client
@ -81,50 +79,32 @@ class NftablesQueue:
self.cmds: List[str] = [] self.cmds: List[str] = []
# list of WLANs requiring update # list of WLANs requiring update
self.updates: SetQueue = SetQueue() 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. Start thread to listen for updates for the provided network.
:param net: network to start checking updates
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
self.last_update_time[net] = time.monotonic() if not self.running:
if self.running: self.running = True
return self.run_thread = threading.Thread(target=self.run, daemon=True)
self.running = True self.run_thread.start()
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. Stop updates for network, when no networks remain, stop update thread.
:param net: network to stop watching updates
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
self.last_update_time.pop(net, None) if self.running:
if self.last_update_time: self.running = False
return
self.running = False
if self.run_thread:
self.updates.put(None) self.updates.put(None)
self.run_thread.join() self.run_thread.join()
self.run_thread = None 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: def run(self) -> None:
""" """
Thread target that looks for networks needing update, and Thread target that looks for networks needing update, and
@ -137,17 +117,13 @@ class NftablesQueue:
net = self.updates.get() net = self.updates.get()
if net is None: if net is None:
break break
if not net.up: self.build_cmds(net)
self.last_update_time[net] = time.monotonic() self.commit(net)
elif self.last_update(net) > self.rate:
with self.lock:
self.build_cmds(net)
self.commit(net)
self.last_update_time[net] = time.monotonic()
def commit(self, net: "CoreNetwork") -> None: def commit(self, net: "CoreNetwork") -> None:
""" """
Commit changes to nftables for the provided network. Commit changes to nftables for the provided network.
:param net: network to commit nftables changes :param net: network to commit nftables changes
:return: nothing :return: nothing
""" """
@ -165,6 +141,7 @@ class NftablesQueue:
def update(self, net: "CoreNetwork") -> None: def update(self, net: "CoreNetwork") -> None:
""" """
Flag this network has an update, so the nftables chain will be rebuilt. Flag this network has an update, so the nftables chain will be rebuilt.
:param net: wlan network :param net: wlan network
:return: nothing :return: nothing
""" """
@ -183,6 +160,7 @@ class NftablesQueue:
def build_cmds(self, net: "CoreNetwork") -> None: def build_cmds(self, net: "CoreNetwork") -> None:
""" """
Inspect linked nodes for a network, and rebuild the nftables chain commands. Inspect linked nodes for a network, and rebuild the nftables chain commands.
:param net: network to build commands for :param net: network to build commands for
:return: nothing :return: nothing
""" """
@ -195,7 +173,7 @@ class NftablesQueue:
self.cmds.append(f"add table bridge {net.brname}") self.cmds.append(f"add table bridge {net.brname}")
self.cmds.append( self.cmds.append(
f"add chain bridge {net.brname} {self.chain} {{type filter hook " 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 # add default rule to accept all traffic not for this bridge
self.cmds.append( self.cmds.append(
@ -300,7 +278,7 @@ class CoreNetwork(CoreNetworkBase):
self.net_client.set_mtu(self.brname, self.mtu) self.net_client.set_mtu(self.brname, self.mtu)
self.has_nftables_chain = False self.has_nftables_chain = False
self.up = True self.up = True
nft_queue.start(self) nft_queue.start()
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -310,7 +288,7 @@ class CoreNetwork(CoreNetworkBase):
""" """
if not self.up: if not self.up:
return return
nft_queue.stop(self) nft_queue.stop()
try: try:
self.net_client.delete_bridge(self.brname) self.net_client.delete_bridge(self.brname)
if self.has_nftables_chain: if self.has_nftables_chain:
@ -400,77 +378,6 @@ class CoreNetwork(CoreNetworkBase):
self.linked[iface1][iface2] = True self.linked[iface1][iface2] = True
nft_queue.update(self) 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: def linknet(self, net: CoreNetworkBase) -> CoreInterface:
""" """
Link this bridge with another by creating a veth pair and installing Link this bridge with another by creating a veth pair and installing
@ -815,41 +722,12 @@ class PtpNet(CoreNetwork):
all_links = [] all_links = []
if len(self.ifaces) != 2: if len(self.ifaces) != 2:
return all_links return all_links
ifaces = self.get_ifaces() ifaces = self.get_ifaces()
iface1 = ifaces[0] iface1 = ifaces[0]
iface2 = ifaces[1] iface2 = ifaces[1]
unidirectional = 0 unidirectional = 0 if iface1.local_options == iface2.local_options else 1
if iface1.getparams() != iface2.getparams(): iface1_data = iface1.get_data()
unidirectional = 1 iface2_data = iface2.get_data()
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)
link_data = LinkData( link_data = LinkData(
message_type=flags, message_type=flags,
type=self.linktype, type=self.linktype,
@ -857,25 +735,23 @@ class PtpNet(CoreNetwork):
node2_id=iface2.node.id, node2_id=iface2.node.id,
iface1=iface1_data, iface1=iface1_data,
iface2=iface2_data, iface2=iface2_data,
options=options_data, options=iface1.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
# build a 2nd link message for the upstream link parameters # build a 2nd link message for the upstream link parameters
# (swap if1 and if2) # (swap if1 and if2)
if unidirectional: 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( link_data = LinkData(
message_type=MessageFlags.NONE, message_type=MessageFlags.NONE,
type=self.linktype, type=self.linktype,
node1_id=iface2.node.id, node1_id=iface2.node.id,
node2_id=iface1.node.id, node2_id=iface1.node.id,
iface1=iface1_data, iface1=InterfaceData(id=iface2_data.id),
iface2=iface2_data, iface2=InterfaceData(id=iface1_data.id),
options=options_data, options=iface2.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
return all_links return all_links

View file

@ -7,14 +7,13 @@ import threading
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple 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.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes, TransportType from core.emulator.enumerations import NodeTypes, TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, UMOUNT from core.executables import MOUNT, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.base import CoreNetworkBase, CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import CoreNetwork
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -143,17 +142,6 @@ class PhysicalNode(CoreNodeBase):
if self.up: if self.up:
self.net_client.device_up(iface.localname) 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: def next_iface_id(self) -> int:
with self.lock: with self.lock:
while self.iface_id in self.ifaces: while self.iface_id in self.ifaces:
@ -245,7 +233,7 @@ class Rj45Node(CoreNodeBase):
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.iface: CoreInterface = CoreInterface( self.iface: CoreInterface = CoreInterface(
session, self, name, name, mtu, server session, name, name, mtu, server, self
) )
self.iface.transport_type = TransportType.RAW self.iface.transport_type = TransportType.RAW
self.lock: threading.RLock = threading.RLock() self.lock: threading.RLock = threading.RLock()
@ -450,3 +438,12 @@ class Rj45Node(CoreNodeBase):
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
raise CoreError("rj45 does not support cmds") 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")

557
daemon/poetry.lock generated
View file

@ -16,16 +16,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "20.1.0" version = "21.4.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev" category = "dev"
optional = false 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] [package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] 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 = ["sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 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]] [[package]]
name = "bcrypt" name = "bcrypt"
@ -62,7 +63,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "1.14.2" version = "1.15.0"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
category = "main" category = "main"
optional = false optional = false
@ -81,15 +82,19 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "click" name = "click"
version = "7.1.2" version = "8.0.3"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "dev" category = "dev"
optional = false 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]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.3" version = "0.4.4"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev" category = "dev"
optional = false optional = false
@ -97,27 +102,26 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "3.0" version = "36.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
cffi = ">=1.8,<1.11.3 || >1.11.3" cffi = ">=1.12"
six = ">=1.4.1"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 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)"] docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
idna = ["idna (>=2.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools_rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"] 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]] [[package]]
name = "dataclasses" name = "dataclasses"
version = "0.7" version = "0.8"
description = "A backport of the dataclasses module for Python 3.6" description = "A backport of the dataclasses module for Python 3.6"
category = "main" category = "main"
optional = false optional = false
@ -125,7 +129,7 @@ python-versions = ">=3.6, <3.7"
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.1" version = "0.3.4"
description = "Distribution utilities" description = "Distribution utilities"
category = "dev" category = "dev"
optional = false optional = false
@ -149,11 +153,15 @@ testing = ["mock (>=2.0.0,<3.0)"]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.0.12" version = "3.4.1"
description = "A platform independent file lock." description = "A platform independent file lock."
category = "dev" category = "dev"
optional = false 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]] [[package]]
name = "flake8" name = "flake8"
@ -194,7 +202,7 @@ protobuf = ">=3.5.0.post1"
[[package]] [[package]]
name = "identify" name = "identify"
version = "1.4.28" version = "1.6.2"
description = "File identification library for Python" description = "File identification library for Python"
category = "dev" category = "dev"
optional = false optional = false
@ -205,32 +213,35 @@ license = ["editdistance"]
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "1.7.0" version = "4.8.3"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5" zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["sphinx", "rst.linker"] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 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]] [[package]]
name = "importlib-resources" name = "importlib-resources"
version = "3.0.0" version = "5.4.0"
description = "Read resources from Python packages" description = "Read resources from Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras] [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]] [[package]]
name = "invoke" name = "invoke"
@ -285,11 +296,11 @@ lingua = ["lingua"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "1.1.1" version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=3.6"
[[package]] [[package]]
name = "mccabe" name = "mccabe"
@ -314,7 +325,7 @@ test = ["pytest", "pytest-cov"]
[[package]] [[package]]
name = "more-itertools" name = "more-itertools"
version = "8.4.0" version = "8.12.0"
description = "More routines for operating on iterables, beyond itertools" description = "More routines for operating on iterables, beyond itertools"
category = "dev" category = "dev"
optional = false optional = false
@ -330,7 +341,7 @@ python-versions = "*"
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.4.0" version = "1.6.0"
description = "Node.js virtual environment builder" description = "Node.js virtual environment builder"
category = "dev" category = "dev"
optional = false optional = false
@ -338,19 +349,18 @@ python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "20.4" version = "21.3"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
six = "*"
[[package]] [[package]]
name = "paramiko" name = "paramiko"
version = "2.7.1" version = "2.9.2"
description = "SSH2 protocol library" description = "SSH2 protocol library"
category = "main" category = "main"
optional = false optional = false
@ -375,6 +385,18 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" 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]] [[package]]
name = "pluggy" name = "pluggy"
version = "0.13.1" version = "0.13.1"
@ -409,22 +431,19 @@ virtualenv = ">=15.2"
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "3.12.2" version = "3.19.4"
description = "Protocol Buffers" description = "Protocol Buffers"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = ">=3.5"
[package.dependencies]
six = ">=1.9"
[[package]] [[package]]
name = "py" name = "py"
version = "1.9.0" version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev" category = "dev"
optional = false 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]] [[package]]
name = "pycodestyle" name = "pycodestyle"
@ -436,7 +455,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.20" version = "2.21"
description = "C parser in Python" description = "C parser in Python"
category = "main" category = "main"
optional = false optional = false
@ -452,15 +471,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pynacl" name = "pynacl"
version = "1.4.0" version = "1.5.0"
description = "Python binding to the Networking and Cryptography (NaCl) library" description = "Python binding to the Networking and Cryptography (NaCl) library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
cffi = ">=1.4.1" cffi = ">=1.4.1"
six = "*"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 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]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "3.0.7"
description = "Python parsing module" description = "Python parsing module"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pyproj" name = "pyproj"
@ -515,7 +536,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]] [[package]]
name = "six" name = "six"
version = "1.15.0" version = "1.16.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
category = "main" category = "main"
optional = false optional = false
@ -523,31 +544,39 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.1" version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language" description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev" category = "dev"
optional = false 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]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.0.31" version = "20.13.1"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false 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] [package.dependencies]
appdirs = ">=1.4.3,<2"
distlib = ">=0.3.1,<1" distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4" filelock = ">=3.2,<4"
importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""}
platformdirs = ">=2,<3"
six = ">=1.9.0,<2" six = ">=1.9.0,<2"
[package.extras] [package.extras]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
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)"] 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]] [[package]]
name = "wcwidth" name = "wcwidth"
@ -559,20 +588,20 @@ python-versions = "*"
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.1.0" version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"] 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] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "ce58fdcb0bde84e407f05bb89472405dd231cdec02ba84cabd47510bee8b3cc2" content-hash = "64ea28583e46b32b3aa2be3627ee8f68c1bbf36622ec6f575062d5059745a6f9"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
@ -584,8 +613,8 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
] ]
attrs = [ attrs = [
{file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
] ]
bcrypt = [ bcrypt = [
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, {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"}, {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"},
] ]
cffi = [ cffi = [
{file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"}, {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"}, {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"}, {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
{file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"}, {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
{file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"}, {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"}, {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"}, {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
{file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"}, {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"}, {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"}, {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
{file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"}, {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
{file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"}, {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
{file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"}, {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"}, {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"}, {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
{file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"}, {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
{file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"}, {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
{file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"}, {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"}, {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"}, {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
{file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"}, {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
{file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"}, {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
{file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"}, {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"}, {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"}, {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
{file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"}, {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
{file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"}, {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
{file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"}, {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 = [ cfgv = [
{file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"},
{file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"},
] ]
click = [ click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
] ]
colorama = [ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"},
{file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"},
{file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"},
{file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"},
{file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"},
{file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"},
{file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"},
{file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"},
{file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"},
{file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"},
{file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"},
{file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"},
{file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"},
{file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"},
{file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"},
{file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"},
{file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"},
{file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"},
{file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, {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 = [ dataclasses = [
{file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
] ]
distlib = [ distlib = [
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
] ]
fabric = [ fabric = [
{file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"},
{file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"},
] ]
filelock = [ filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"},
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"},
] ]
flake8 = [ flake8 = [
{file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"}, {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"}, {file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"},
] ]
identify = [ identify = [
{file = "identify-1.4.28-py2.py3-none-any.whl", hash = "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6"}, {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"},
{file = "identify-1.4.28.tar.gz", hash = "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"}, {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"},
] ]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
] ]
importlib-resources = [ importlib-resources = [
{file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"},
{file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"},
] ]
invoke = [ invoke = [
{file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, {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"}, {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"},
] ]
markupsafe = [ markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, {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-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, {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-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, {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-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {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-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, {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-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {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-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, {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-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, {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-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, {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 = [ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {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"}, {file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"},
] ]
more-itertools = [ more-itertools = [
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
] ]
netaddr = [ netaddr = [
{file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"}, {file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"},
{file = "netaddr-0.7.19.tar.gz", hash = "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"}, {file = "netaddr-0.7.19.tar.gz", hash = "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"},
] ]
nodeenv = [ nodeenv = [
{file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
{file = "nodeenv-1.4.0.tar.gz", hash = "sha256:26941644654d8dd5378720e38f62a3bac5f9240811fb3b8913d2663a17baa91c"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
] ]
packaging = [ packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
] ]
paramiko = [ paramiko = [
{file = "paramiko-2.7.1-py2.py3-none-any.whl", hash = "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"}, {file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"},
{file = "paramiko-2.7.1.tar.gz", hash = "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f"}, {file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"},
] ]
pillow = [ pillow = [
{file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, {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-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
{file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, {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 = [ pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, {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"}, {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"},
] ]
protobuf = [ protobuf = [
{file = "protobuf-3.12.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c"}, {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"},
{file = "protobuf-3.12.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776"}, {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"},
{file = "protobuf-3.12.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a"}, {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"},
{file = "protobuf-3.12.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3"}, {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"},
{file = "protobuf-3.12.2-cp35-cp35m-win32.whl", hash = "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07"}, {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"},
{file = "protobuf-3.12.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925"}, {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"},
{file = "protobuf-3.12.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea"}, {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"},
{file = "protobuf-3.12.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e"}, {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"},
{file = "protobuf-3.12.2-cp36-cp36m-win32.whl", hash = "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122"}, {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"},
{file = "protobuf-3.12.2-cp36-cp36m-win_amd64.whl", hash = "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f"}, {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"},
{file = "protobuf-3.12.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0"}, {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"},
{file = "protobuf-3.12.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907"}, {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"},
{file = "protobuf-3.12.2-cp37-cp37m-win32.whl", hash = "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2"}, {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"},
{file = "protobuf-3.12.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e"}, {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"},
{file = "protobuf-3.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828"}, {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"},
{file = "protobuf-3.12.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9"}, {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"},
{file = "protobuf-3.12.2-py2.py3-none-any.whl", hash = "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb"}, {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"},
{file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"}, {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 = [ py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
] ]
pycodestyle = [ pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
] ]
pycparser = [ pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
] ]
pyflakes = [ pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
] ]
pynacl = [ pynacl = [
{file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
{file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
{file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, {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.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
{file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
{file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
{file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
{file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
{file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
{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"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
] ]
pyproj = [ pyproj = [
{file = "pyproj-2.6.1.post1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:457ad3856014ac26af1d86def6dc8cf69c1fa377b6e2fd6e97912d51cf66bdbe"}, {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"}, {file = "PyYAML-5.4.tar.gz", hash = "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a"},
] ]
six = [ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
] ]
toml = [ toml = [
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, {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 = [ virtualenv = [
{file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"},
{file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"},
] ]
wcwidth = [ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
] ]
zipp = [ zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
] ]

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "core" name = "core"
version = "8.0.0" version = "8.1.0"
description = "CORE Common Open Research Emulator" description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"] authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause" license = "BSD-2-Clause"
@ -25,7 +25,7 @@ lxml = "4.6.5"
mako = "1.1.3" mako = "1.1.3"
netaddr = "0.7.19" netaddr = "0.7.19"
pillow = "8.3.2" pillow = "8.3.2"
protobuf = "3.12.2" protobuf = "3.19.4"
pyproj = "2.6.1.post1" pyproj = "2.6.1.post1"
pyyaml = "5.4" pyyaml = "5.4"

View file

@ -130,12 +130,6 @@ class TestCore:
assert 0 in node1.ifaces assert 0 in node1.ifaces
assert 0 in node2.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 # delete interface and test that if no longer exists
node1.delete_iface(0) node1.delete_iface(0)
assert 0 not in node1.ifaces assert 0 not in node1.ifaces

View file

@ -1,10 +1,18 @@
from typing import Tuple from typing import Tuple
import pytest
from core.emulator.data import IpPrefixes, LinkOptions from core.emulator.data import IpPrefixes, LinkOptions
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.network import SwitchNode 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( def create_ptp_network(
session: Session, ip_prefixes: IpPrefixes session: Session, ip_prefixes: IpPrefixes
@ -25,7 +33,7 @@ def create_ptp_network(
class TestLinks: 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 # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
@ -33,11 +41,19 @@ class TestLinks:
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# when # 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 # then
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None
assert iface2 is not None
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): def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -46,11 +62,16 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
# when # 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 # then
assert node2.links() assert node2.links()
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert iface is not None
assert iface.local_options == LINK_OPTIONS
assert iface.has_local_netem
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -59,11 +80,16 @@ class TestLinks:
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# when # 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 # then
assert node1.links() assert node1.links()
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface is not None
assert iface.local_options == LINK_OPTIONS
assert iface.has_local_netem
def test_add_net_to_net(self, session): def test_add_net_to_net(self, session):
# given # given
@ -71,147 +97,151 @@ class TestLinks:
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
# when # when
session.add_link(node1.id, node2.id) iface, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
# then # then
assert node1.links() assert node1.links()
assert iface is not None
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
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 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): def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
delay = 50
bandwidth = 5000000
loss = 25
dup = 25
jitter = 10
buffer = 100
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
session.add_link(node1.id, node2.id, iface1_data) iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
iface1 = node1.get_iface(iface1_data.id) assert iface1.local_options != LINK_OPTIONS
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
# when # when
options = LinkOptions(
delay=delay,
bandwidth=bandwidth,
loss=loss,
dup=dup,
jitter=jitter,
buffer=buffer,
)
session.update_link( 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 # then
assert iface1.getparam("delay") == delay assert iface1.local_options == LINK_OPTIONS
assert iface1.getparam("bw") == bandwidth assert iface1.has_local_netem
assert iface1.getparam("loss") == loss
assert iface1.getparam("duplicate") == dup
assert iface1.getparam("jitter") == jitter
assert iface1.getparam("buffer") == buffer
def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
delay = 50
bandwidth = 5000000
loss = 25
dup = 25
jitter = 10
buffer = 100
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface2_data=iface2_data) _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
iface2 = node2.get_iface(iface2_data.id) assert iface2.local_options != LINK_OPTIONS
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
# when # when
options = LinkOptions(
delay=delay,
bandwidth=bandwidth,
loss=loss,
dup=dup,
jitter=jitter,
buffer=buffer,
)
session.update_link( 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 # then
assert iface2.getparam("delay") == delay assert iface2.local_options == LINK_OPTIONS
assert iface2.getparam("bw") == bandwidth assert iface2.has_local_netem
assert iface2.getparam("loss") == loss
assert iface2.getparam("duplicate") == dup
assert iface2.getparam("jitter") == jitter
assert iface2.getparam("buffer") == buffer
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
delay = 50
bandwidth = 5000000
loss = 25
dup = 25
jitter = 10
buffer = 100
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
iface1 = node1.get_iface(iface1_data.id) assert iface1.local_options != LINK_OPTIONS
iface2 = node2.get_iface(iface2_data.id) assert iface2.local_options != LINK_OPTIONS
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
# when # when
options = LinkOptions( session.update_link(
delay=delay, node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS
bandwidth=bandwidth,
loss=loss,
dup=dup,
jitter=jitter,
buffer=buffer,
) )
session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options)
# then # then
assert iface1.getparam("delay") == delay assert iface1.local_options == LINK_OPTIONS
assert iface1.getparam("bw") == bandwidth assert iface1.has_local_netem
assert iface1.getparam("loss") == loss assert iface2.local_options == LINK_OPTIONS
assert iface1.getparam("duplicate") == dup assert iface2.has_local_netem
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): 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 iface1.local_options != LINK_OPTIONS
# when
session.update_link(node1.id, node2.id, options=LINK_OPTIONS)
# then
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 # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
@ -255,3 +285,82 @@ class TestLinks:
# then # then
assert iface2_data.id not in node2.ifaces 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)

View file

@ -168,13 +168,12 @@ class ExampleService(ConfigService):
] ]
def get_text_template(self, name: str) -> str: def get_text_template(self, name: str) -> str:
if name == "example-start.sh": return """
return """ # sample script 1
# sample script 1 # node id(${node.id}) name(${node.name})
# node id(${node.id}) name(${node.name}) # config: ${config}
# config: ${config} echo hello
echo hello """
"""
``` ```
#### Validation Mode #### Validation Mode

View file

@ -24,16 +24,6 @@ Verified:
* Ubuntu - 18.04, 20.04 * Ubuntu - 18.04, 20.04
* CentOS - 7.8, 8.0 * 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 > **NOTE:** CentOS 8 does not have the netem kernel mod available by default
CentOS 8 Enabled netem: CentOS 8 Enabled netem:
@ -130,6 +120,14 @@ First we will need to clone and navigate to the CORE repo.
# clone CORE repo # clone CORE repo
git clone https://github.com/coreemu/core.git git clone https://github.com/coreemu/core.git
cd core cd core
# install dependencies to run installation task
./setup.sh
# run the following or open a new terminal
source ~/.bashrc
# Ubuntu
inv install
# CentOS
./install.sh -p /usr
``` ```
First you can use `setup.sh` as a convenience to install tooling for running invoke tasks: First you can use `setup.sh` as a convenience to install tooling for running invoke tasks:
@ -140,7 +138,7 @@ First you can use `setup.sh` as a convenience to install tooling for running inv
* python3, pip, venv * python3, pip, venv
* pipx 0.16.4 via pip * pipx 0.16.4 via pip
* invoke 1.4.1 via pipx * invoke 1.4.1 via pipx
* poetry 1.1.7 via pipx * poetry 1.1.12 via pipx
Then you can run `inv install <options>`: Then you can run `inv install <options>`:
* installs system dependencies for building core * installs system dependencies for building core

View file

@ -1,8 +1,5 @@
#!/bin/bash #!/bin/bash
# exit on error
set -e
# install pre-reqs using yum/apt # install pre-reqs using yum/apt
if command -v apt &> /dev/null if command -v apt &> /dev/null
then then
@ -23,4 +20,4 @@ python3 -m pip install --user pipx==0.16.4
python3 -m pipx ensurepath python3 -m pipx ensurepath
export PATH=$PATH:~/.local/bin export PATH=$PATH:~/.local/bin
pipx install invoke==1.4.1 pipx install invoke==1.4.1
pipx install poetry==1.1.7 pipx install poetry==1.1.12