Merge pull request #1104 from GNS3/filters_api

Support packet filtering for VPCS
This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2017-07-08 18:50:00 +07:00
commit 5bdc239064
25 files changed
+527 -270

No files matched your search

+2 -1
View File
@@ -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):
+24
View File
@@ -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):
+13 -1
View File
@@ -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):
+15 -1
View File
@@ -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):
"""
+121 -2
View File
@@ -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
}
+2
View File
@@ -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)
+31 -2
View File
@@ -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)
-230
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)
+23
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"
}
+2 -2
View File
@@ -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
}
+4 -1
View File
@@ -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
+2 -2
View File
@@ -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: