diff --git a/.gitignore b/.gitignore
index c1434d71..385ea50d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,4 +55,5 @@ startup.vpcs
.gns3_shell_history
# Virtualenv
-env
\ No newline at end of file
+env.ropeproject
+.ropeproject
diff --git a/docs/curl.rst b/docs/curl.rst
index ea92f409..02cc2b04 100644
--- a/docs/curl.rst
+++ b/docs/curl.rst
@@ -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
#################
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 3da68aea..9b25df9f 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -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.
diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py
index 18dffa26..0885db90 100644
--- a/gns3server/compute/base_manager.py
+++ b/gns3server/compute/base_manager.py
@@ -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):
diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py
index 1afa05cf..47640cf6 100644
--- a/gns3server/compute/base_node.py
+++ b/gns3server/compute/base_node.py
@@ -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):
diff --git a/gns3server/compute/nios/nio_udp.py b/gns3server/compute/nios/nio_udp.py
index a87875fe..96811a94 100644
--- a/gns3server/compute/nios/nio_udp.py
+++ b/gns3server/compute/nios/nio_udp.py
@@ -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):
diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py
index 3bc90264..8de816aa 100644
--- a/gns3server/compute/vpcs/vpcs_vm.py
+++ b/gns3server/compute/vpcs/vpcs_vm.py
@@ -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):
"""
diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py
index 90ec12fc..9e202bfb 100644
--- a/gns3server/controller/link.py
+++ b/gns3server/controller/link.py
@@ -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
}
diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py
index cb60d8df..6936e95c 100644
--- a/gns3server/controller/project.py
+++ b/gns3server/controller/project.py
@@ -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)
diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py
index 5b87ec24..798f97c5 100644
--- a/gns3server/controller/udp_link.py
+++ b/gns3server/controller/udp_link.py
@@ -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):
"""
diff --git a/gns3server/handlers/api/compute/vpcs_handler.py b/gns3server/handlers/api/compute/vpcs_handler.py
index d4096adf..cfd1a210 100644
--- a/gns3server/handlers/api/compute/vpcs_handler.py
+++ b/gns3server/handlers/api/compute/vpcs_handler.py
@@ -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={
diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py
index 4854ad90..df229e01 100644
--- a/gns3server/handlers/api/controller/link_handler.py
+++ b/gns3server/handlers/api/controller/link_handler.py
@@ -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)
diff --git a/gns3server/handlers/api/vpcs_handler.py b/gns3server/handlers/api/vpcs_handler.py
deleted file mode 100644
index 6af82ad7..00000000
--- a/gns3server/handlers/api/vpcs_handler.py
+++ /dev/null
@@ -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 .
-
-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)
diff --git a/gns3server/schemas/filter.py b/gns3server/schemas/filter.py
new file mode 100644
index 00000000..d7f09972
--- /dev/null
+++ b/gns3server/schemas/filter.py
@@ -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 .
+
+
+FILTER_OBJECT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Packet filter. This allow to simulate latency and errors",
+ "type": "object"
+}
diff --git a/gns3server/schemas/link.py b/gns3server/schemas/link.py
index 877c4a89..d3e66dee 100644
--- a/gns3server/schemas/link.py
+++ b/gns3server/schemas/link.py
@@ -16,7 +16,7 @@
# along with this program. If not, see .
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
}
diff --git a/gns3server/schemas/nio.py b/gns3server/schemas/nio.py
index 5efe4840..e45211a7 100644
--- a/gns3server/schemas/nio.py
+++ b/gns3server/schemas/nio.py
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+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
diff --git a/gns3server/ubridge/hypervisor.py b/gns3server/ubridge/hypervisor.py
index 9deb7b83..a7b1123c 100644
--- a/gns3server/ubridge/hypervisor.py
+++ b/gns3server/ubridge/hypervisor.py
@@ -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:
diff --git a/tests/compute/builtin/nodes/test_cloud.py b/tests/compute/builtin/nodes/test_cloud.py
index 43fee80d..f8eb4a9a 100644
--- a/tests/compute/builtin/nodes/test_cloud.py
+++ b/tests/compute/builtin/nodes/test_cloud.py
@@ -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
diff --git a/tests/compute/dynamips/test_ethernet_switch.py b/tests/compute/dynamips/test_ethernet_switch.py
index 1520a4be..e316da7f 100644
--- a/tests/compute/dynamips/test_ethernet_switch.py
+++ b/tests/compute/dynamips/test_ethernet_switch.py
@@ -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)
diff --git a/tests/compute/test_base_node.py b/tests/compute/test_base_node.py
index 20a3ee06..e42f3e98 100644
--- a/tests/compute/test_base_node.py
+++ b/tests/compute/test_base_node.py
@@ -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")
diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py
index 40022544..24638fdb 100644
--- a/tests/controller/test_link.py
+++ b/tests/controller/test_link.py
@@ -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
diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py
index d56325f2..2ca0d061 100644
--- a/tests/controller/test_project.py
+++ b/tests/controller/test_project.py
@@ -617,4 +617,4 @@ def test_add_iou_node_and_check_if_gets_application_id(project, async_run):
node = async_run(project.add_node(
compute, "test", None, node_type="iou", application_id=333, properties={"startup_config": "test.cfg"}))
assert mocked_get_app_id.called
- assert node.properties['application_id'] == 333
\ No newline at end of file
+ assert node.properties['application_id'] == 333
diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py
index 023630bb..3ab6581f 100644
--- a/tests/controller/test_udp_link.py
+++ b/tests/controller/test_udp_link.py
@@ -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)
diff --git a/tests/handlers/api/compute/test_vpcs.py b/tests/handlers/api/compute/test_vpcs.py
index 85456e5e..265a11fd 100644
--- a/tests/handlers/api/compute/test_vpcs.py
+++ b/tests/handlers/api/compute/test_vpcs.py
@@ -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):
diff --git a/tests/handlers/api/controller/test_link.py b/tests/handlers/api/controller/test_link.py
index a7e3194e..e1ff8fa6 100644
--- a/tests/handlers/api/controller/test_link.py
+++ b/tests/handlers/api/controller/test_link.py
@@ -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