Support packet filtering for VPCS

https://github.com/GNS3/gns3-gui/issues/765
This commit is contained in:
Julien Duponchelle 2017-06-30 10:22:30 +02:00
parent 8f72356bab
commit 08423eff96
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
25 changed files with 526 additions and 269 deletions

3
.gitignore vendored
View File

@ -55,4 +55,5 @@ startup.vpcs
.gns3_shell_history .gns3_shell_history
# Virtualenv # Virtualenv
env env.ropeproject
.ropeproject

View File

@ -222,6 +222,15 @@ This will display a red square in the middle of your topologies:
Tips: you can embed png/jpg... by using a base64 encoding in the SVG. Tips: you can embed png/jpg... by using a base64 encoding in the SVG.
Add filter to the link
######################
Filter allow you to add error on a link.
.. code-block:: shell-session
curl -X PUT "http://localhost:3080/v2/projects/b8c070f7-f34c-4b7b-ba6f-be3d26ed073f/links/007f2177-6790-4e1b-ac28-41fa226b2a06" -d '{"filters": {"frequency_drop": [5]}}'
Creation of nodes Creation of nodes
################# #################

View File

@ -62,3 +62,8 @@ Symbol are the icon used for nodes.
Scene Scene
----- -----
The drawing area The drawing area
Filter
------
Packet filter this allow to add latency or packet drop.

View File

@ -373,7 +373,8 @@ class BaseManager:
sock.connect(sa) sock.connect(sa)
except OSError as e: except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIOUDP(lport, rhost, rport) filters = nio_settings.get("filters", [])
nio = NIOUDP(lport, rhost, rport, filters)
elif nio_settings["type"] == "nio_tap": elif nio_settings["type"] == "nio_tap":
tap_device = nio_settings["tap_device"] tap_device = nio_settings["tap_device"]
# if not is_interface_up(tap_device): # if not is_interface_up(tap_device):

View File

@ -586,6 +586,30 @@ class BaseNode:
pcap_file=destination_nio.pcap_output_file)) pcap_file=destination_nio.pcap_output_file))
yield from self._ubridge_send('bridge start {name}'.format(name=bridge_name)) yield from self._ubridge_send('bridge start {name}'.format(name=bridge_name))
yield from self._ubridge_apply_filters(bridge_name, destination_nio.filters)
@asyncio.coroutine
def _update_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio):
yield from self._ubridge_apply_filters(bridge_name, destination_nio.filters)
@asyncio.coroutine
def _ubridge_apply_filters(self, bridge_name, filters):
"""
Apply filter like rate limiting
:param bridge_name: bridge name in uBridge
:param filters: Array of filter dictionnary
"""
yield from self._ubridge_send('bridge reset_packet_filters ' + bridge_name)
i = 0
for (type, values) in filters.items():
cmd = "bridge add_packet_filter {bridge_name} {filter_name} {filter_type} {filter_value}".format(
bridge_name=bridge_name,
filter_name="filter" + str(i),
filter_type=type,
filter_value=" ".join([str(v) for v in values]))
yield from self._ubridge_send(cmd)
i += 1
@asyncio.coroutine @asyncio.coroutine
def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=True): def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=True):

View File

@ -32,12 +32,24 @@ class NIOUDP(NIO):
:param rport: remote port number :param rport: remote port number
""" """
def __init__(self, lport, rhost, rport): def __init__(self, lport, rhost, rport, filters):
super().__init__() super().__init__()
self._lport = lport self._lport = lport
self._rhost = rhost self._rhost = rhost
self._rport = rport self._rport = rport
self._filters = filters
@property
def filters(self):
"""
Return the list of filter on this NIO
"""
return self._filters
@filters.setter
def filters(self, val):
self._filters = val
@property @property
def lport(self): def lport(self):

View File

@ -73,6 +73,10 @@ class VPCSVM(BaseNode):
self.startup_script = startup_script self.startup_script = startup_script
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
@property
def ethernet_adapter(self):
return self._ethernet_adapter
@asyncio.coroutine @asyncio.coroutine
def close(self): def close(self):
""" """
@ -387,6 +391,16 @@ class VPCSVM(BaseNode):
return nio return nio
@asyncio.coroutine
def port_update_nio_binding(self, port_number, nio):
if not self._ethernet_adapter.port_exists(port_number):
raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
if self.ubridge:
yield from self._update_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio)
elif self.is_running():
raise VPCSError("Sorry, adding a link to a started VPCS instance is not supported without using uBridge.")
@asyncio.coroutine @asyncio.coroutine
def port_remove_nio_binding(self, port_number): def port_remove_nio_binding(self, port_number):
""" """

View File

@ -26,6 +26,68 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FILTERS = [
{
"type": "frequency_drop",
"name": "Frequency drop",
"description": "It will drop everything with a -1 frequency, drop every Nth packet with a positive frequency, or drop nothing",
"parameters": [
{
"name": "Frequency",
"minimum": -1,
"maximum": 32767,
"unit": "th packet"
}
]
},
{
"type": "packet_loss",
"name": "Packet loss",
"description": "The percentage represents the chance for a packet to be lost",
"parameters": [
{
"name": "Frequency",
"minimum": 0,
"maximum": 100,
"unit": "%"
}
]
},
{
"type": "delay",
"name": "Delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay",
"parameters": [
{
"name": "Delay",
"minimum": 0,
"maximum": 32767,
"unit": "ms"
},
{
"name": "Jitter",
"minimum": 0,
"maximum": 32767,
"unit": "ms"
}
]
},
{
"type": "corrupt",
"name": "Corrupt",
"description": "The percentage represents the chance for a packet to be corrupt",
"parameters": [
{
"name": "Frequency",
"minimum": 0,
"maximum": 100,
"unit": "%"
}
]
}
]
class Link: class Link:
""" """
Base class for links. Base class for links.
@ -44,6 +106,32 @@ class Link:
self._streaming_pcap = None self._streaming_pcap = None
self._created = False self._created = False
self._link_type = "ethernet" self._link_type = "ethernet"
self._filters = {}
@property
def filters(self):
"""
Get an array of filters
"""
return self._filters
@asyncio.coroutine
def update_filters(self, filters):
"""
Modify the filters list.
Filter with value 0 will be dropped because not active
"""
new_filters = {}
for (filter, values) in filters.items():
values = [int(v) for v in values]
if len(values) != 0 and values[0] != 0:
new_filters[filter] = values
if new_filters != self.filters:
self._filters = new_filters
if self._created:
yield from self.update()
@property @property
def created(self): def created(self):
@ -127,6 +215,13 @@ class Link:
raise NotImplementedError raise NotImplementedError
@asyncio.coroutine
def update(self):
"""
Update a link
"""
raise NotImplementedError
@asyncio.coroutine @asyncio.coroutine
def delete(self): def delete(self):
""" """
@ -230,6 +325,28 @@ class Link:
else: else:
return None return None
def available_filters(self):
"""
Return the list of filters compatible with this link
:returns: Array of filters
"""
filter_node = self._get_filter_node()
if filter_node:
return FILTERS
return []
def _get_filter_node(self):
"""
Return the node where the filter will run
:returns: None if no node support filtering else the node
"""
for node in self._nodes:
if node["node"].node_type in ('vpcs', ):
return node["node"]
return None
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Link): if not isinstance(other, Link):
return False return False
@ -253,7 +370,8 @@ class Link:
if topology_dump: if topology_dump:
return { return {
"nodes": res, "nodes": res,
"link_id": self._id "link_id": self._id,
"filters": self._filters
} }
return { return {
"nodes": res, "nodes": res,
@ -262,5 +380,6 @@ class Link:
"capturing": self._capturing, "capturing": self._capturing,
"capture_file_name": self._capture_file_name, "capture_file_name": self._capture_file_name,
"capture_file_path": self.capture_file_path, "capture_file_path": self.capture_file_path,
"link_type": self._link_type "link_type": self._link_type,
"filters": self._filters
} }

View File

@ -665,6 +665,8 @@ class Project:
for node_link in link_data["nodes"]: for node_link in link_data["nodes"]:
node = self.get_node(node_link["node_id"]) node = self.get_node(node_link["node_id"])
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False) yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
if "filters" in link_data:
yield from link.update_filters(link_data["filters"])
for drawing_data in topology.get("drawings", []): for drawing_data in topology.get("drawings", []):
yield from self.add_drawing(dump=False, **drawing_data) yield from self.add_drawing(dump=False, **drawing_data)

View File

@ -62,12 +62,21 @@ class UDPLink(Link):
response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id)) response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
self._node2_port = response.json["udp_port"] self._node2_port = response.json["udp_port"]
node1_filters = {}
node2_filters = {}
filter_node = self._get_filter_node()
if filter_node == node1:
node1_filters = self._filters
elif filter_node == node2:
node2_filters = self._filters
# Create the tunnel on both side # Create the tunnel on both side
self._link_data.append({ self._link_data.append({
"lport": self._node1_port, "lport": self._node1_port,
"rhost": node2_host, "rhost": node2_host,
"rport": self._node2_port, "rport": self._node2_port,
"type": "nio_udp" "type": "nio_udp",
"filters": node1_filters
}) })
yield from node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120) yield from node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
@ -75,7 +84,8 @@ class UDPLink(Link):
"lport": self._node2_port, "lport": self._node2_port,
"rhost": node1_host, "rhost": node1_host,
"rport": self._node1_port, "rport": self._node1_port,
"type": "nio_udp" "type": "nio_udp",
"filters": node2_filters
}) })
try: try:
yield from node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120) yield from node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120)
@ -85,6 +95,25 @@ class UDPLink(Link):
raise e raise e
self._created = True self._created = True
@asyncio.coroutine
def update(self):
if len(self._link_data) == 0:
return
node1 = self._nodes[0]["node"]
node2 = self._nodes[1]["node"]
filter_node = self._get_filter_node()
if node1 == filter_node:
adapter_number1 = self._nodes[0]["adapter_number"]
port_number1 = self._nodes[0]["port_number"]
self._link_data[0]["filters"] = self._filters
yield from node1.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
elif node2 == filter_node:
adapter_number2 = self._nodes[1]["adapter_number"]
port_number2 = self._nodes[1]["port_number"]
self._link_data[1]["filters"] = self._filters
yield from node2.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=221)
@asyncio.coroutine @asyncio.coroutine
def delete(self): def delete(self):
""" """

View File

@ -224,6 +224,33 @@ class VPCSHandler:
response.set_status(201) response.set_status(201)
response.json(nio) response.json(nio)
@Route.put(
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port from where the nio should be updated"
},
status_codes={
201: "NIO updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
input=NIO_SCHEMA,
output=NIO_SCHEMA,
description="Update a NIO from a VPCS instance")
def update_nio(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
nio = vm.ethernet_adapter.get_nio(int(request.match_info["port_number"]))
if "filters" in request.json and nio:
nio.filters = request.json["filters"]
yield from vm.port_update_nio_binding(int(request.match_info["port_number"]), nio)
response.set_status(201)
response.json(request.json)
@Route.delete( @Route.delete(
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={ parameters={

View File

@ -62,6 +62,7 @@ class LinkHandler:
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"]) project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
link = yield from project.add_link() link = yield from project.add_link()
yield from link.update_filters(request.json.get("filters", {}))
try: try:
for node in request.json["nodes"]: for node in request.json["nodes"]:
yield from link.add_node(project.get_node(node["node_id"]), yield from link.add_node(project.get_node(node["node_id"]),
@ -74,6 +75,24 @@ class LinkHandler:
response.set_status(201) response.set_status(201)
response.json(link) response.json(link)
@Route.get(
r"/projects/{project_id}/links/{link_id}/available_filters",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
200: "List of filters",
400: "Invalid request"
},
description="Return the list of filters available for this link")
def list_filters(request, response):
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
response.set_status(200)
response.json(link.available_filters())
@Route.put( @Route.put(
r"/projects/{project_id}/links/{link_id}", r"/projects/{project_id}/links/{link_id}",
parameters={ parameters={
@ -91,7 +110,9 @@ class LinkHandler:
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"]) project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"]) link = project.get_link(request.match_info["link_id"])
yield from link.update_nodes(request.json["nodes"]) yield from link.update_filters(request.json.get("filters", {}))
if "nodes" in request.json:
yield from link.update_nodes(request.json["nodes"])
response.set_status(201) response.set_status(201)
response.json(link) response.json(link)

View File

@ -1,230 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from aiohttp.web import HTTPConflict
from ...web.route import Route
from ...schemas.nio import NIO_SCHEMA
from ...schemas.vpcs import VPCS_CREATE_SCHEMA
from ...schemas.vpcs import VPCS_UPDATE_SCHEMA
from ...schemas.vpcs import VPCS_OBJECT_SCHEMA
from ...modules.vpcs import VPCS
class VPCSHandler:
"""
API entry points for VPCS.
"""
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms",
parameters={
"project_id": "UUID for the project"
},
status_codes={
201: "Instance created",
400: "Invalid request",
409: "Conflict"
},
description="Create a new VPCS instance",
input=VPCS_CREATE_SCHEMA,
output=VPCS_OBJECT_SCHEMA)
def create(request, response):
vpcs = VPCS.instance()
vm = yield from vpcs.create_vm(request.json["name"],
request.match_info["project_id"],
request.json.get("vm_id"),
console=request.json.get("console"),
startup_script=request.json.get("startup_script"))
response.set_status(201)
response.json(vm)
@classmethod
@Route.get(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
200: "Success",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Get a VPCS instance",
output=VPCS_OBJECT_SCHEMA)
def show(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
response.json(vm)
@classmethod
@Route.put(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist",
409: "Conflict"
},
description="Update a VPCS instance",
input=VPCS_UPDATE_SCHEMA,
output=VPCS_OBJECT_SCHEMA)
def update(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
vm.name = request.json.get("name", vm.name)
vm.console = request.json.get("console", vm.console)
vm.startup_script = request.json.get("startup_script", vm.startup_script)
response.json(vm)
@classmethod
@Route.delete(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Delete a VPCS instance")
def delete(request, response):
yield from VPCS.instance().delete_vm(request.match_info["vm_id"])
response.set_status(204)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/start",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance started",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Start a VPCS instance",
output=VPCS_OBJECT_SCHEMA)
def start(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.start()
response.json(vm)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/stop",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance stopped",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Stop a VPCS instance")
def stop(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.stop()
response.set_status(204)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/reload",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a VPCS instance")
def reload(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.reload()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port where the nio should be added"
},
status_codes={
201: "NIO created",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Add a NIO to a VPCS instance",
input=NIO_SCHEMA,
output=NIO_SCHEMA)
def create_nio(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
nio_type = request.json["type"]
if nio_type not in ("nio_udp", "nio_tap"):
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
nio = vpcs_manager.create_nio(vm.vpcs_path(), request.json)
vm.port_add_nio_binding(int(request.match_info["port_number"]), nio)
response.set_status(201)
response.json(nio)
@classmethod
@Route.delete(
r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port from where the nio should be removed"
},
status_codes={
204: "NIO deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Remove a NIO from a VPCS instance")
def delete_nio(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
vm.port_remove_nio_binding(int(request.match_info["port_number"]))
response.set_status(204)

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
#
# Copyright (C) 2017 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
FILTER_OBJECT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Packet filter. This allow to simulate latency and errors",
"type": "object"
}

View File

@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from .label import LABEL_OBJECT_SCHEMA from .label import LABEL_OBJECT_SCHEMA
from .filter import FILTER_OBJECT_SCHEMA
LINK_OBJECT_SCHEMA = { LINK_OBJECT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
@ -64,6 +64,7 @@ LINK_OBJECT_SCHEMA = {
"additionalProperties": False "additionalProperties": False
} }
}, },
"filters": FILTER_OBJECT_SCHEMA,
"capturing": { "capturing": {
"description": "Read only property. True if a capture running on the link", "description": "Read only property. True if a capture running on the link",
"type": "boolean" "type": "boolean"
@ -81,7 +82,6 @@ LINK_OBJECT_SCHEMA = {
"enum": ["ethernet", "serial"] "enum": ["ethernet", "serial"]
} }
}, },
"required": ["nodes"],
"additionalProperties": False "additionalProperties": False
} }

View File

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from .filter import FILTER_OBJECT_SCHEMA
NIO_SCHEMA = { NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
@ -43,7 +45,8 @@ NIO_SCHEMA = {
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} },
"filters": FILTER_OBJECT_SCHEMA
}, },
"required": ["type", "lport", "rhost", "rport"], "required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False "additionalProperties": False

View File

@ -138,8 +138,8 @@ class Hypervisor(UBridgeHypervisor):
match = re.search("ubridge version ([0-9a-z\.]+)", output) match = re.search("ubridge version ([0-9a-z\.]+)", output)
if match: if match:
self._version = match.group(1) self._version = match.group(1)
if parse_version(self._version) < parse_version("0.9.7"): if parse_version(self._version) < parse_version("0.9.12"):
raise UbridgeError("uBridge executable version must be >= 0.9.7") raise UbridgeError("uBridge executable version must be >= 0.9.12")
else: else:
raise UbridgeError("Could not determine uBridge version for {}".format(self._path)) raise UbridgeError("Could not determine uBridge version for {}".format(self._path))
except (OSError, subprocess.SubprocessError) as e: except (OSError, subprocess.SubprocessError) as e:

View File

@ -26,7 +26,7 @@ from tests.utils import asyncio_patch
@pytest.fixture @pytest.fixture
def nio(): def nio():
return NIOUDP(4242, "127.0.0.1", 4343) return NIOUDP(4242, "127.0.0.1", 4343, [])
@pytest.fixture @pytest.fixture

View File

@ -24,9 +24,9 @@ def test_arp_command(async_run):
node = AsyncioMagicMock() node = AsyncioMagicMock()
node.name = "Test" node.name = "Test"
node.nios = {} node.nios = {}
node.nios[0] = NIOUDP(55, "127.0.0.1", 56) node.nios[0] = NIOUDP(55, "127.0.0.1", 56, [])
node.nios[0].name = "Ethernet0" node.nios[0].name = "Ethernet0"
node.nios[1] = NIOUDP(55, "127.0.0.1", 56) node.nios[1] = NIOUDP(55, "127.0.0.1", 56, [])
node.nios[1].name = "Ethernet1" node.nios[1].name = "Ethernet1"
node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"]) node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"])
console = EthernetSwitchConsole(node) console = EthernetSwitchConsole(node)

View File

@ -19,7 +19,7 @@ import pytest
import aiohttp import aiohttp
import asyncio import asyncio
import os import os
from tests.utils import asyncio_patch from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@ -28,6 +28,7 @@ from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.vpcs.vpcs_error import VPCSError from gns3server.compute.vpcs.vpcs_error import VPCSError
from gns3server.compute.error import NodeError from gns3server.compute.error import NodeError
from gns3server.compute.vpcs import VPCS from gns3server.compute.vpcs import VPCS
from gns3server.compute.nios.nio_udp import NIOUDP
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -121,3 +122,26 @@ def test_change_aux_port(node, port_manager):
node.aux = port2 node.aux = port2
assert node.aux == port2 assert node.aux == port2
port_manager.reserve_tcp_port(port1, node.project) port_manager.reserve_tcp_port(port1, node.project)
def test_update_ubridge_udp_connection(node, async_run):
filters = [{
"type": "latency",
"value": 10
}]
snio = NIOUDP(1245, "localhost", 1246, [])
dnio = NIOUDP(1245, "localhost", 1244, filters)
with asyncio_patch("gns3server.compute.base_node.BaseNode._ubridge_apply_filters") as mock:
async_run(node._update_ubridge_udp_connection('VPCS-10', snio, dnio))
mock.assert_called_with("VPCS-10", filters)
def test_ubridge_apply_filters(node, async_run):
filters = {
"latency": [10]
}
node._ubridge_send = AsyncioMagicMock()
async_run(node._ubridge_apply_filters("VPCS-10", filters))
node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10")
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 latency 10")

View File

@ -230,6 +230,7 @@ def test_json(async_run, project, compute, link):
} }
} }
], ],
"filters": {},
"link_type": "ethernet", "link_type": "ethernet",
"capturing": False, "capturing": False,
"capture_file_name": None, "capture_file_name": None,
@ -262,7 +263,8 @@ def test_json(async_run, project, compute, link):
'style': 'font-size: 10; font-style: Verdana' 'style': 'font-size: 10; font-style: Verdana'
} }
} }
] ],
"filters": {}
} }
@ -348,3 +350,49 @@ def test_delete(async_run, project, compute):
async_run(link.delete()) async_run(link.delete())
assert link not in node2.link assert link not in node2.link
def test_update_filters(async_run, project, compute):
node1 = Node(project, compute, "node1", node_type="qemu")
node1._ports = [EthernetPort("E0", 0, 0, 4)]
link = Link(project)
link.create = AsyncioMagicMock()
link._project.controller.notification.emit = MagicMock()
project.dump = AsyncioMagicMock()
async_run(link.add_node(node1, 0, 4))
node2 = Node(project, compute, "node2", node_type="qemu")
node2._ports = [EthernetPort("E0", 0, 0, 4)]
async_run(link.add_node(node2, 0, 4))
link.update = AsyncioMagicMock()
assert link._created
async_run(link.update_filters({
"packet_loss": ["10"],
"delay": ["50", "10"],
"frequency_drop": ["0"]
}))
assert link.filters == {
"packet_loss": [10],
"delay": [50, 10]
}
assert link.update.called
def test_available_filters(async_run, project, compute):
node1 = Node(project, compute, "node1", node_type="qemu")
node1._ports = [EthernetPort("E0", 0, 0, 4)]
link = Link(project)
link.create = AsyncioMagicMock()
assert link.available_filters() == []
# Qemu is not supported should return 0 filters
async_run(link.add_node(node1, 0, 4))
assert link.available_filters() == []
node2 = Node(project, compute, "node2", node_type="vpcs")
node2._ports = [EthernetPort("E0", 0, 0, 4)]
async_run(link.add_node(node2, 0, 4))
assert len(link.available_filters()) > 0

View File

@ -612,4 +612,4 @@ def test_add_iou_node_and_check_if_gets_application_id(project, async_run):
node = async_run(project.add_node( node = async_run(project.add_node(
compute, "test", None, node_type="iou", application_id=333, properties={"startup_config": "test.cfg"})) compute, "test", None, node_type="iou", application_id=333, properties={"startup_config": "test.cfg"}))
assert mocked_get_app_id.called assert mocked_get_app_id.called
assert node.properties['application_id'] == 333 assert node.properties['application_id'] == 333

View File

@ -19,7 +19,7 @@ import pytest
import asyncio import asyncio
import aiohttp import aiohttp
from unittest.mock import MagicMock from unittest.mock import MagicMock
from tests.utils import asyncio_patch, AsyncioMagicMock from tests.utils import AsyncioMagicMock
from gns3server.controller.project import Project from gns3server.controller.project import Project
from gns3server.controller.udp_link import UDPLink from gns3server.controller.udp_link import UDPLink
@ -52,6 +52,7 @@ def test_create(async_run, project):
link = UDPLink(project) link = UDPLink(project)
async_run(link.add_node(node1, 0, 4)) async_run(link.add_node(node1, 0, 4))
async_run(link.update_filters({"latency": [10]}))
@asyncio.coroutine @asyncio.coroutine
def compute1_callback(path, data={}, **kwargs): def compute1_callback(path, data={}, **kwargs):
@ -83,13 +84,15 @@ def test_create(async_run, project):
"lport": 1024, "lport": 1024,
"rhost": "192.168.1.2", "rhost": "192.168.1.2",
"rport": 2048, "rport": 2048,
"type": "nio_udp" "type": "nio_udp",
"filters": {"latency": [10]}
}, timeout=120) }, timeout=120)
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
"lport": 2048, "lport": 2048,
"rhost": "192.168.1.1", "rhost": "192.168.1.1",
"rport": 1024, "rport": 1024,
"type": "nio_udp" "type": "nio_udp",
"filters": {}
}, timeout=120) }, timeout=120)
@ -147,13 +150,15 @@ def test_create_one_side_failure(async_run, project):
"lport": 1024, "lport": 1024,
"rhost": "192.168.1.2", "rhost": "192.168.1.2",
"rport": 2048, "rport": 2048,
"type": "nio_udp" "type": "nio_udp",
"filters": {}
}, timeout=120) }, timeout=120)
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
"lport": 2048, "lport": 2048,
"rhost": "192.168.1.1", "rhost": "192.168.1.1",
"rport": 1024, "rport": 1024,
"type": "nio_udp" "type": "nio_udp",
"filters": {}
}, timeout=120) }, timeout=120)
# The link creation has failed we rollback the nio # The link creation has failed we rollback the nio
compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120) compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120)
@ -302,3 +307,77 @@ def test_node_updated(project, async_run):
node_vpcs._status = "stopped" node_vpcs._status = "stopped"
async_run(link.node_updated(node_vpcs)) async_run(link.node_updated(node_vpcs))
assert link.stop_capture.called assert link.stop_capture.called
def test_update(async_run, project):
compute1 = MagicMock()
compute2 = MagicMock()
node1 = Node(project, compute1, "node1", node_type="vpcs")
node1._ports = [EthernetPort("E0", 0, 0, 4)]
node2 = Node(project, compute2, "node2", node_type="vpcs")
node2._ports = [EthernetPort("E0", 0, 3, 1)]
@asyncio.coroutine
def subnet_callback(compute2):
"""
Fake subnet callback
"""
return ("192.168.1.1", "192.168.1.2")
compute1.get_ip_on_same_subnet.side_effect = subnet_callback
link = UDPLink(project)
async_run(link.add_node(node1, 0, 4))
async_run(link.update_filters({"latency": [10]}))
@asyncio.coroutine
def compute1_callback(path, data={}, **kwargs):
"""
Fake server
"""
if "/ports/udp" in path:
response = MagicMock()
response.json = {"udp_port": 1024}
return response
@asyncio.coroutine
def compute2_callback(path, data={}, **kwargs):
"""
Fake server
"""
if "/ports/udp" in path:
response = MagicMock()
response.json = {"udp_port": 2048}
return response
compute1.post.side_effect = compute1_callback
compute1.host = "example.com"
compute2.post.side_effect = compute2_callback
compute2.host = "example.org"
async_run(link.add_node(node2, 3, 1))
compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
"lport": 1024,
"rhost": "192.168.1.2",
"rport": 2048,
"type": "nio_udp",
"filters": {"latency": [10]}
}, timeout=120)
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
"lport": 2048,
"rhost": "192.168.1.1",
"rport": 1024,
"type": "nio_udp",
"filters": {}
}, timeout=120)
assert link.created
async_run(link.update_filters({"drop": [5]}))
compute1.put.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
"lport": 1024,
"rhost": "192.168.1.2",
"rport": 2048,
"type": "nio_udp",
"filters": {"drop": [5]}
}, timeout=120)

View File

@ -74,6 +74,20 @@ def test_vpcs_nio_create_udp(http_compute, vm):
assert response.json["type"] == "nio_udp" assert response.json["type"] == "nio_udp"
def test_vpcs_nio_update_udp(http_compute, vm):
response = http_compute.put("/projects/{project_id}/vpcs/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]),
{
"type": "nio_udp",
"lport": 4242,
"rport": 4343,
"rhost": "127.0.0.1",
"filters": {}},
example=True)
assert response.status == 201
assert response.route == "/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
assert response.json["type"] == "nio_udp"
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
def test_vpcs_nio_create_tap(http_compute, vm, ethernet_device): def test_vpcs_nio_create_tap(http_compute, vm, ethernet_device):
with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True): with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True):

View File

@ -33,7 +33,7 @@ from gns3server.handlers.api.controller.project_handler import ProjectHandler
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server.controller.ports.ethernet_port import EthernetPort from gns3server.controller.ports.ethernet_port import EthernetPort
from gns3server.controller.node import Node from gns3server.controller.node import Node
from gns3server.controller.link import Link from gns3server.controller.link import Link, FILTERS
@pytest.fixture @pytest.fixture
@ -59,6 +59,11 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
node2._ports = [EthernetPort("E0", 0, 2, 4)] node2._ports = [EthernetPort("E0", 0, 2, 4)]
filters = {
"latency": [10],
"frequency_drop": [50]
}
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = http_controller.post("/projects/{}/links".format(project.id), { response = http_controller.post("/projects/{}/links".format(project.id), {
"nodes": [ "nodes": [
@ -77,7 +82,8 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
"adapter_number": 2, "adapter_number": 2,
"port_number": 4 "port_number": 4
} }
] ],
"filters": filters
}, example=True) }, example=True)
assert mock.called assert mock.called
assert response.status == 201 assert response.status == 201
@ -85,6 +91,7 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
assert len(response.json["nodes"]) == 2 assert len(response.json["nodes"]) == 2
assert response.json["nodes"][0]["label"]["x"] == 42 assert response.json["nodes"][0]["label"]["x"] == 42
assert len(project.links) == 1 assert len(project.links) == 1
assert list(project.links.values())[0].filters == filters
def test_create_link_failure(http_controller, tmpdir, project, compute, async_run): def test_create_link_failure(http_controller, tmpdir, project, compute, async_run):
@ -135,6 +142,11 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run):
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
node2._ports = [EthernetPort("E0", 0, 2, 4)] node2._ports = [EthernetPort("E0", 0, 2, 4)]
filters = {
"latency": 10,
"frequency_drop": 50
}
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = http_controller.post("/projects/{}/links".format(project.id), { response = http_controller.post("/projects/{}/links".format(project.id), {
"nodes": [ "nodes": [
@ -174,10 +186,12 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run):
"adapter_number": 2, "adapter_number": 2,
"port_number": 4 "port_number": 4
} }
] ],
"filters": filters
}) })
assert response.status == 201 assert response.status == 201
assert response.json["nodes"][0]["label"]["x"] == 64 assert response.json["nodes"][0]["label"]["x"] == 64
assert list(project.links.values())[0].filters == filters
def test_list_link(http_controller, tmpdir, project, compute, async_run): def test_list_link(http_controller, tmpdir, project, compute, async_run):
@ -190,24 +204,31 @@ def test_list_link(http_controller, tmpdir, project, compute, async_run):
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
node2._ports = [EthernetPort("E0", 0, 2, 4)] node2._ports = [EthernetPort("E0", 0, 2, 4)]
filters = {
"latency": 10,
"frequency_drop": 50
}
nodes = [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
]
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = http_controller.post("/projects/{}/links".format(project.id), { response = http_controller.post("/projects/{}/links".format(project.id), {
"nodes": [ "nodes": nodes,
{ "filters": filters
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
]
}) })
response = http_controller.get("/projects/{}/links".format(project.id), example=True) response = http_controller.get("/projects/{}/links".format(project.id), example=True)
assert response.status == 200 assert response.status == 200
assert len(response.json) == 1 assert len(response.json) == 1
assert response.json[0]["filters"] == filters
def test_start_capture(http_controller, tmpdir, project, compute, async_run): def test_start_capture(http_controller, tmpdir, project, compute, async_run):
@ -258,3 +279,14 @@ def test_delete_link(http_controller, tmpdir, project, compute, async_run):
response = http_controller.delete("/projects/{}/links/{}".format(project.id, link.id), example=True) response = http_controller.delete("/projects/{}/links/{}".format(project.id, link.id), example=True)
assert mock.called assert mock.called
assert response.status == 204 assert response.status == 204
def test_list_filters(http_controller, tmpdir, project, async_run):
link = Link(project)
project._links = {link.id: link}
with patch("gns3server.controller.link.Link.available_filters", return_value=FILTERS) as mock:
response = http_controller.get("/projects/{}/links/{}/available_filters".format(project.id, link.id), example=True)
assert mock.called
assert response.status == 200
assert response.json == FILTERS