mirror of
https://github.com/GNS3/gns3-server.git
synced 2024-12-20 21:33:09 +00:00
Merge pull request #1104 from GNS3/filters_api
Support packet filtering for VPCS
This commit is contained in:
commit
5bdc239064
3
.gitignore
vendored
3
.gitignore
vendored
@ -55,4 +55,5 @@ startup.vpcs
|
||||
.gns3_shell_history
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
env.ropeproject
|
||||
.ropeproject
|
||||
|
@ -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.
|
||||
|
||||
|
||||
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
|
||||
#################
|
||||
|
||||
|
@ -62,3 +62,8 @@ Symbol are the icon used for nodes.
|
||||
Scene
|
||||
-----
|
||||
The drawing area
|
||||
|
||||
|
||||
Filter
|
||||
------
|
||||
Packet filter this allow to add latency or packet drop.
|
||||
|
@ -373,7 +373,8 @@ class BaseManager:
|
||||
sock.connect(sa)
|
||||
except OSError as 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":
|
||||
tap_device = nio_settings["tap_device"]
|
||||
# if not is_interface_up(tap_device):
|
||||
|
@ -586,6 +586,30 @@ class BaseNode:
|
||||
pcap_file=destination_nio.pcap_output_file))
|
||||
|
||||
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 (filter_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=filter_type,
|
||||
filter_value=" ".join([str(v) for v in values]))
|
||||
yield from self._ubridge_send(cmd)
|
||||
i += 1
|
||||
|
||||
@asyncio.coroutine
|
||||
def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=True):
|
||||
|
@ -32,12 +32,24 @@ class NIOUDP(NIO):
|
||||
:param rport: remote port number
|
||||
"""
|
||||
|
||||
def __init__(self, lport, rhost, rport):
|
||||
def __init__(self, lport, rhost, rport, filters):
|
||||
|
||||
super().__init__()
|
||||
self._lport = lport
|
||||
self._rhost = rhost
|
||||
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
|
||||
def lport(self):
|
||||
|
@ -73,6 +73,10 @@ class VPCSVM(BaseNode):
|
||||
self.startup_script = startup_script
|
||||
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
|
||||
|
||||
@property
|
||||
def ethernet_adapter(self):
|
||||
return self._ethernet_adapter
|
||||
|
||||
@asyncio.coroutine
|
||||
def close(self):
|
||||
"""
|
||||
@ -377,7 +381,7 @@ class VPCSVM(BaseNode):
|
||||
if self.ubridge:
|
||||
yield from self._add_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.")
|
||||
raise VPCSError("Sorry, updating a link to a started VPCS instance is not supported without using uBridge.")
|
||||
|
||||
self._ethernet_adapter.add_nio(port_number, nio)
|
||||
log.info('VPCS "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name,
|
||||
@ -387,6 +391,16 @@ class VPCSVM(BaseNode):
|
||||
|
||||
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
|
||||
def port_remove_nio_binding(self, port_number):
|
||||
"""
|
||||
|
@ -26,6 +26,68 @@ import logging
|
||||
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": "Chance",
|
||||
"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": "Latency",
|
||||
"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 corrupted",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Chance",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"unit": "%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class Link:
|
||||
"""
|
||||
Base class for links.
|
||||
@ -44,6 +106,32 @@ class Link:
|
||||
self._streaming_pcap = None
|
||||
self._created = False
|
||||
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
|
||||
def created(self):
|
||||
@ -127,6 +215,13 @@ class Link:
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
@asyncio.coroutine
|
||||
def update(self):
|
||||
"""
|
||||
Update a link
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self):
|
||||
"""
|
||||
@ -230,6 +325,28 @@ class Link:
|
||||
else:
|
||||
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):
|
||||
if not isinstance(other, Link):
|
||||
return False
|
||||
@ -253,7 +370,8 @@ class Link:
|
||||
if topology_dump:
|
||||
return {
|
||||
"nodes": res,
|
||||
"link_id": self._id
|
||||
"link_id": self._id,
|
||||
"filters": self._filters
|
||||
}
|
||||
return {
|
||||
"nodes": res,
|
||||
@ -262,5 +380,6 @@ class Link:
|
||||
"capturing": self._capturing,
|
||||
"capture_file_name": self._capture_file_name,
|
||||
"capture_file_path": self.capture_file_path,
|
||||
"link_type": self._link_type
|
||||
"link_type": self._link_type,
|
||||
"filters": self._filters
|
||||
}
|
||||
|
@ -746,6 +746,8 @@ class Project:
|
||||
for node_link in link_data["nodes"]:
|
||||
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)
|
||||
if "filters" in link_data:
|
||||
yield from link.update_filters(link_data["filters"])
|
||||
for drawing_data in topology.get("drawings", []):
|
||||
yield from self.add_drawing(dump=False, **drawing_data)
|
||||
|
||||
|
@ -62,12 +62,21 @@ class UDPLink(Link):
|
||||
response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
||||
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
|
||||
self._link_data.append({
|
||||
"lport": self._node1_port,
|
||||
"rhost": node2_host,
|
||||
"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)
|
||||
|
||||
@ -75,7 +84,8 @@ class UDPLink(Link):
|
||||
"lport": self._node2_port,
|
||||
"rhost": node1_host,
|
||||
"rport": self._node1_port,
|
||||
"type": "nio_udp"
|
||||
"type": "nio_udp",
|
||||
"filters": node2_filters
|
||||
})
|
||||
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)
|
||||
@ -85,6 +95,25 @@ class UDPLink(Link):
|
||||
raise e
|
||||
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
|
||||
def delete(self):
|
||||
"""
|
||||
|
@ -224,6 +224,33 @@ class VPCSHandler:
|
||||
response.set_status(201)
|
||||
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(
|
||||
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
|
@ -62,6 +62,7 @@ class LinkHandler:
|
||||
|
||||
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
||||
link = yield from project.add_link()
|
||||
yield from link.update_filters(request.json.get("filters", {}))
|
||||
try:
|
||||
for node in request.json["nodes"]:
|
||||
yield from link.add_node(project.get_node(node["node_id"]),
|
||||
@ -74,6 +75,24 @@ class LinkHandler:
|
||||
response.set_status(201)
|
||||
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(
|
||||
r"/projects/{project_id}/links/{link_id}",
|
||||
parameters={
|
||||
@ -91,7 +110,9 @@ class LinkHandler:
|
||||
|
||||
project = yield from Controller.instance().get_loaded_project(request.match_info["project_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.json(link)
|
||||
|
||||
|
@ -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)
|
23
gns3server/schemas/filter.py
Normal file
23
gns3server/schemas/filter.py
Normal 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"
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .label import LABEL_OBJECT_SCHEMA
|
||||
|
||||
from .filter import FILTER_OBJECT_SCHEMA
|
||||
|
||||
LINK_OBJECT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
@ -64,6 +64,7 @@ LINK_OBJECT_SCHEMA = {
|
||||
"additionalProperties": False
|
||||
}
|
||||
},
|
||||
"filters": FILTER_OBJECT_SCHEMA,
|
||||
"capturing": {
|
||||
"description": "Read only property. True if a capture running on the link",
|
||||
"type": "boolean"
|
||||
@ -81,7 +82,6 @@ LINK_OBJECT_SCHEMA = {
|
||||
"enum": ["ethernet", "serial"]
|
||||
}
|
||||
},
|
||||
"required": ["nodes"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
# 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 .filter import FILTER_OBJECT_SCHEMA
|
||||
|
||||
|
||||
NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
@ -43,7 +45,8 @@ NIO_SCHEMA = {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
}
|
||||
},
|
||||
"filters": FILTER_OBJECT_SCHEMA
|
||||
},
|
||||
"required": ["type", "lport", "rhost", "rport"],
|
||||
"additionalProperties": False
|
||||
|
@ -138,8 +138,8 @@ class Hypervisor(UBridgeHypervisor):
|
||||
match = re.search("ubridge version ([0-9a-z\.]+)", output)
|
||||
if match:
|
||||
self._version = match.group(1)
|
||||
if parse_version(self._version) < parse_version("0.9.7"):
|
||||
raise UbridgeError("uBridge executable version must be >= 0.9.7")
|
||||
if parse_version(self._version) < parse_version("0.9.12"):
|
||||
raise UbridgeError("uBridge executable version must be >= 0.9.12")
|
||||
else:
|
||||
raise UbridgeError("Could not determine uBridge version for {}".format(self._path))
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
|
@ -26,7 +26,7 @@ from tests.utils import asyncio_patch
|
||||
|
||||
@pytest.fixture
|
||||
def nio():
|
||||
return NIOUDP(4242, "127.0.0.1", 4343)
|
||||
return NIOUDP(4242, "127.0.0.1", 4343, [])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -24,9 +24,9 @@ def test_arp_command(async_run):
|
||||
node = AsyncioMagicMock()
|
||||
node.name = "Test"
|
||||
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[1] = NIOUDP(55, "127.0.0.1", 56)
|
||||
node.nios[1] = NIOUDP(55, "127.0.0.1", 56, [])
|
||||
node.nios[1].name = "Ethernet1"
|
||||
node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"])
|
||||
console = EthernetSwitchConsole(node)
|
||||
|
@ -19,7 +19,7 @@ import pytest
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import os
|
||||
from tests.utils import asyncio_patch
|
||||
from tests.utils import asyncio_patch, AsyncioMagicMock
|
||||
|
||||
|
||||
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.error import NodeError
|
||||
from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.nios.nio_udp import NIOUDP
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -121,3 +122,26 @@ def test_change_aux_port(node, port_manager):
|
||||
node.aux = port2
|
||||
assert node.aux == port2
|
||||
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")
|
||||
|
@ -230,6 +230,7 @@ def test_json(async_run, project, compute, link):
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": {},
|
||||
"link_type": "ethernet",
|
||||
"capturing": False,
|
||||
"capture_file_name": None,
|
||||
@ -262,7 +263,8 @@ def test_json(async_run, project, compute, link):
|
||||
'style': 'font-size: 10; font-style: Verdana'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": {}
|
||||
}
|
||||
|
||||
|
||||
@ -348,3 +350,49 @@ def test_delete(async_run, project, compute):
|
||||
|
||||
async_run(link.delete())
|
||||
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
|
||||
|
@ -19,7 +19,7 @@ import pytest
|
||||
import asyncio
|
||||
import aiohttp
|
||||
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.udp_link import UDPLink
|
||||
@ -52,6 +52,7 @@ def test_create(async_run, project):
|
||||
|
||||
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):
|
||||
@ -83,13 +84,15 @@ def test_create(async_run, project):
|
||||
"lport": 1024,
|
||||
"rhost": "192.168.1.2",
|
||||
"rport": 2048,
|
||||
"type": "nio_udp"
|
||||
"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"
|
||||
"type": "nio_udp",
|
||||
"filters": {}
|
||||
}, timeout=120)
|
||||
|
||||
|
||||
@ -147,13 +150,15 @@ def test_create_one_side_failure(async_run, project):
|
||||
"lport": 1024,
|
||||
"rhost": "192.168.1.2",
|
||||
"rport": 2048,
|
||||
"type": "nio_udp"
|
||||
"type": "nio_udp",
|
||||
"filters": {}
|
||||
}, 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"
|
||||
"type": "nio_udp",
|
||||
"filters": {}
|
||||
}, timeout=120)
|
||||
# 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)
|
||||
@ -302,3 +307,77 @@ def test_node_updated(project, async_run):
|
||||
node_vpcs._status = "stopped"
|
||||
async_run(link.node_updated(node_vpcs))
|
||||
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)
|
||||
|
@ -74,6 +74,20 @@ def test_vpcs_nio_create_udp(http_compute, vm):
|
||||
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")
|
||||
def test_vpcs_nio_create_tap(http_compute, vm, ethernet_device):
|
||||
with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True):
|
||||
|
@ -33,7 +33,7 @@ from gns3server.handlers.api.controller.project_handler import ProjectHandler
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.controller.ports.ethernet_port import EthernetPort
|
||||
from gns3server.controller.node import Node
|
||||
from gns3server.controller.link import Link
|
||||
from gns3server.controller.link import Link, FILTERS
|
||||
|
||||
|
||||
@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._ports = [EthernetPort("E0", 0, 2, 4)]
|
||||
|
||||
filters = {
|
||||
"latency": [10],
|
||||
"frequency_drop": [50]
|
||||
}
|
||||
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||
"nodes": [
|
||||
@ -77,7 +82,8 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
|
||||
"adapter_number": 2,
|
||||
"port_number": 4
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": filters
|
||||
}, example=True)
|
||||
assert mock.called
|
||||
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 response.json["nodes"][0]["label"]["x"] == 42
|
||||
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):
|
||||
@ -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._ports = [EthernetPort("E0", 0, 2, 4)]
|
||||
|
||||
filters = {
|
||||
"latency": 10,
|
||||
"frequency_drop": 50
|
||||
}
|
||||
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||
"nodes": [
|
||||
@ -174,10 +186,12 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run):
|
||||
"adapter_number": 2,
|
||||
"port_number": 4
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": filters
|
||||
})
|
||||
assert response.status == 201
|
||||
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):
|
||||
@ -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._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:
|
||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": node1.id,
|
||||
"adapter_number": 0,
|
||||
"port_number": 3
|
||||
},
|
||||
{
|
||||
"node_id": node2.id,
|
||||
"adapter_number": 2,
|
||||
"port_number": 4
|
||||
}
|
||||
]
|
||||
"nodes": nodes,
|
||||
"filters": filters
|
||||
})
|
||||
response = http_controller.get("/projects/{}/links".format(project.id), example=True)
|
||||
assert response.status == 200
|
||||
assert len(response.json) == 1
|
||||
assert response.json[0]["filters"] == filters
|
||||
|
||||
|
||||
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)
|
||||
assert mock.called
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user