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
|
.gns3_shell_history
|
||||||
|
|
||||||
# Virtualenv
|
# 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.
|
Tips: you can embed png/jpg... by using a base64 encoding in the SVG.
|
||||||
|
|
||||||
|
|
||||||
|
Add filter to the link
|
||||||
|
######################
|
||||||
|
|
||||||
|
Filter allow you to add error on a link.
|
||||||
|
|
||||||
|
.. code-block:: shell-session
|
||||||
|
curl -X PUT "http://localhost:3080/v2/projects/b8c070f7-f34c-4b7b-ba6f-be3d26ed073f/links/007f2177-6790-4e1b-ac28-41fa226b2a06" -d '{"filters": {"frequency_drop": [5]}}'
|
||||||
|
|
||||||
|
|
||||||
Creation of nodes
|
Creation of nodes
|
||||||
#################
|
#################
|
||||||
|
|
||||||
|
@ -62,3 +62,8 @@ Symbol are the icon used for nodes.
|
|||||||
Scene
|
Scene
|
||||||
-----
|
-----
|
||||||
The drawing area
|
The drawing area
|
||||||
|
|
||||||
|
|
||||||
|
Filter
|
||||||
|
------
|
||||||
|
Packet filter this allow to add latency or packet drop.
|
||||||
|
@ -373,7 +373,8 @@ class BaseManager:
|
|||||||
sock.connect(sa)
|
sock.connect(sa)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||||
nio = NIOUDP(lport, rhost, rport)
|
filters = nio_settings.get("filters", [])
|
||||||
|
nio = NIOUDP(lport, rhost, rport, filters)
|
||||||
elif nio_settings["type"] == "nio_tap":
|
elif nio_settings["type"] == "nio_tap":
|
||||||
tap_device = nio_settings["tap_device"]
|
tap_device = nio_settings["tap_device"]
|
||||||
# if not is_interface_up(tap_device):
|
# if not is_interface_up(tap_device):
|
||||||
|
@ -586,6 +586,30 @@ class BaseNode:
|
|||||||
pcap_file=destination_nio.pcap_output_file))
|
pcap_file=destination_nio.pcap_output_file))
|
||||||
|
|
||||||
yield from self._ubridge_send('bridge start {name}'.format(name=bridge_name))
|
yield from self._ubridge_send('bridge start {name}'.format(name=bridge_name))
|
||||||
|
yield from self._ubridge_apply_filters(bridge_name, destination_nio.filters)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _update_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio):
|
||||||
|
yield from self._ubridge_apply_filters(bridge_name, destination_nio.filters)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _ubridge_apply_filters(self, bridge_name, filters):
|
||||||
|
"""
|
||||||
|
Apply filter like rate limiting
|
||||||
|
|
||||||
|
:param bridge_name: bridge name in uBridge
|
||||||
|
:param filters: Array of filter dictionnary
|
||||||
|
"""
|
||||||
|
yield from self._ubridge_send('bridge reset_packet_filters ' + bridge_name)
|
||||||
|
i = 0
|
||||||
|
for (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
|
@asyncio.coroutine
|
||||||
def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=True):
|
def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=True):
|
||||||
|
@ -32,12 +32,24 @@ class NIOUDP(NIO):
|
|||||||
:param rport: remote port number
|
:param rport: remote port number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lport, rhost, rport):
|
def __init__(self, lport, rhost, rport, filters):
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._lport = lport
|
self._lport = lport
|
||||||
self._rhost = rhost
|
self._rhost = rhost
|
||||||
self._rport = rport
|
self._rport = rport
|
||||||
|
self._filters = filters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filters(self):
|
||||||
|
"""
|
||||||
|
Return the list of filter on this NIO
|
||||||
|
"""
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
@filters.setter
|
||||||
|
def filters(self, val):
|
||||||
|
self._filters = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lport(self):
|
def lport(self):
|
||||||
|
@ -73,6 +73,10 @@ class VPCSVM(BaseNode):
|
|||||||
self.startup_script = startup_script
|
self.startup_script = startup_script
|
||||||
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
|
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ethernet_adapter(self):
|
||||||
|
return self._ethernet_adapter
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def close(self):
|
def close(self):
|
||||||
"""
|
"""
|
||||||
@ -377,7 +381,7 @@ class VPCSVM(BaseNode):
|
|||||||
if self.ubridge:
|
if self.ubridge:
|
||||||
yield from self._add_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio)
|
yield from self._add_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio)
|
||||||
elif self.is_running():
|
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)
|
self._ethernet_adapter.add_nio(port_number, nio)
|
||||||
log.info('VPCS "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name,
|
log.info('VPCS "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name,
|
||||||
@ -387,6 +391,16 @@ class VPCSVM(BaseNode):
|
|||||||
|
|
||||||
return nio
|
return nio
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def port_update_nio_binding(self, port_number, nio):
|
||||||
|
if not self._ethernet_adapter.port_exists(port_number):
|
||||||
|
raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
|
||||||
|
port_number=port_number))
|
||||||
|
if self.ubridge:
|
||||||
|
yield from self._update_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio)
|
||||||
|
elif self.is_running():
|
||||||
|
raise VPCSError("Sorry, adding a link to a started VPCS instance is not supported without using uBridge.")
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def port_remove_nio_binding(self, port_number):
|
def port_remove_nio_binding(self, port_number):
|
||||||
"""
|
"""
|
||||||
|
@ -26,6 +26,68 @@ import logging
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
FILTERS = [
|
||||||
|
{
|
||||||
|
"type": "frequency_drop",
|
||||||
|
"name": "Frequency drop",
|
||||||
|
"description": "It will drop everything with a -1 frequency, drop every Nth packet with a positive frequency, or drop nothing",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Frequency",
|
||||||
|
"minimum": -1,
|
||||||
|
"maximum": 32767,
|
||||||
|
"unit": "th packet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "packet_loss",
|
||||||
|
"name": "Packet loss",
|
||||||
|
"description": "The percentage represents the chance for a packet to be lost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "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:
|
class Link:
|
||||||
"""
|
"""
|
||||||
Base class for links.
|
Base class for links.
|
||||||
@ -44,6 +106,32 @@ class Link:
|
|||||||
self._streaming_pcap = None
|
self._streaming_pcap = None
|
||||||
self._created = False
|
self._created = False
|
||||||
self._link_type = "ethernet"
|
self._link_type = "ethernet"
|
||||||
|
self._filters = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filters(self):
|
||||||
|
"""
|
||||||
|
Get an array of filters
|
||||||
|
"""
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def update_filters(self, filters):
|
||||||
|
"""
|
||||||
|
Modify the filters list.
|
||||||
|
|
||||||
|
Filter with value 0 will be dropped because not active
|
||||||
|
"""
|
||||||
|
new_filters = {}
|
||||||
|
for (filter, values) in filters.items():
|
||||||
|
values = [int(v) for v in values]
|
||||||
|
if len(values) != 0 and values[0] != 0:
|
||||||
|
new_filters[filter] = values
|
||||||
|
|
||||||
|
if new_filters != self.filters:
|
||||||
|
self._filters = new_filters
|
||||||
|
if self._created:
|
||||||
|
yield from self.update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created(self):
|
def created(self):
|
||||||
@ -127,6 +215,13 @@ class Link:
|
|||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Update a link
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
@ -230,6 +325,28 @@ class Link:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def available_filters(self):
|
||||||
|
"""
|
||||||
|
Return the list of filters compatible with this link
|
||||||
|
|
||||||
|
:returns: Array of filters
|
||||||
|
"""
|
||||||
|
filter_node = self._get_filter_node()
|
||||||
|
if filter_node:
|
||||||
|
return FILTERS
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _get_filter_node(self):
|
||||||
|
"""
|
||||||
|
Return the node where the filter will run
|
||||||
|
|
||||||
|
:returns: None if no node support filtering else the node
|
||||||
|
"""
|
||||||
|
for node in self._nodes:
|
||||||
|
if node["node"].node_type in ('vpcs', ):
|
||||||
|
return node["node"]
|
||||||
|
return None
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, Link):
|
if not isinstance(other, Link):
|
||||||
return False
|
return False
|
||||||
@ -253,7 +370,8 @@ class Link:
|
|||||||
if topology_dump:
|
if topology_dump:
|
||||||
return {
|
return {
|
||||||
"nodes": res,
|
"nodes": res,
|
||||||
"link_id": self._id
|
"link_id": self._id,
|
||||||
|
"filters": self._filters
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"nodes": res,
|
"nodes": res,
|
||||||
@ -262,5 +380,6 @@ class Link:
|
|||||||
"capturing": self._capturing,
|
"capturing": self._capturing,
|
||||||
"capture_file_name": self._capture_file_name,
|
"capture_file_name": self._capture_file_name,
|
||||||
"capture_file_path": self.capture_file_path,
|
"capture_file_path": self.capture_file_path,
|
||||||
"link_type": self._link_type
|
"link_type": self._link_type,
|
||||||
|
"filters": self._filters
|
||||||
}
|
}
|
||||||
|
@ -746,6 +746,8 @@ class Project:
|
|||||||
for node_link in link_data["nodes"]:
|
for node_link in link_data["nodes"]:
|
||||||
node = self.get_node(node_link["node_id"])
|
node = self.get_node(node_link["node_id"])
|
||||||
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
|
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
|
||||||
|
if "filters" in link_data:
|
||||||
|
yield from link.update_filters(link_data["filters"])
|
||||||
for drawing_data in topology.get("drawings", []):
|
for drawing_data in topology.get("drawings", []):
|
||||||
yield from self.add_drawing(dump=False, **drawing_data)
|
yield from self.add_drawing(dump=False, **drawing_data)
|
||||||
|
|
||||||
|
@ -62,12 +62,21 @@ class UDPLink(Link):
|
|||||||
response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
||||||
self._node2_port = response.json["udp_port"]
|
self._node2_port = response.json["udp_port"]
|
||||||
|
|
||||||
|
node1_filters = {}
|
||||||
|
node2_filters = {}
|
||||||
|
filter_node = self._get_filter_node()
|
||||||
|
if filter_node == node1:
|
||||||
|
node1_filters = self._filters
|
||||||
|
elif filter_node == node2:
|
||||||
|
node2_filters = self._filters
|
||||||
|
|
||||||
# Create the tunnel on both side
|
# Create the tunnel on both side
|
||||||
self._link_data.append({
|
self._link_data.append({
|
||||||
"lport": self._node1_port,
|
"lport": self._node1_port,
|
||||||
"rhost": node2_host,
|
"rhost": node2_host,
|
||||||
"rport": self._node2_port,
|
"rport": self._node2_port,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": node1_filters
|
||||||
})
|
})
|
||||||
yield from node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
|
yield from node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
|
||||||
|
|
||||||
@ -75,7 +84,8 @@ class UDPLink(Link):
|
|||||||
"lport": self._node2_port,
|
"lport": self._node2_port,
|
||||||
"rhost": node1_host,
|
"rhost": node1_host,
|
||||||
"rport": self._node1_port,
|
"rport": self._node1_port,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": node2_filters
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
yield from node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120)
|
yield from node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120)
|
||||||
@ -85,6 +95,25 @@ class UDPLink(Link):
|
|||||||
raise e
|
raise e
|
||||||
self._created = True
|
self._created = True
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def update(self):
|
||||||
|
if len(self._link_data) == 0:
|
||||||
|
return
|
||||||
|
node1 = self._nodes[0]["node"]
|
||||||
|
node2 = self._nodes[1]["node"]
|
||||||
|
filter_node = self._get_filter_node()
|
||||||
|
|
||||||
|
if node1 == filter_node:
|
||||||
|
adapter_number1 = self._nodes[0]["adapter_number"]
|
||||||
|
port_number1 = self._nodes[0]["port_number"]
|
||||||
|
self._link_data[0]["filters"] = self._filters
|
||||||
|
yield from node1.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
|
||||||
|
elif node2 == filter_node:
|
||||||
|
adapter_number2 = self._nodes[1]["adapter_number"]
|
||||||
|
port_number2 = self._nodes[1]["port_number"]
|
||||||
|
self._link_data[1]["filters"] = self._filters
|
||||||
|
yield from node2.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=221)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
|
@ -224,6 +224,33 @@ class VPCSHandler:
|
|||||||
response.set_status(201)
|
response.set_status(201)
|
||||||
response.json(nio)
|
response.json(nio)
|
||||||
|
|
||||||
|
@Route.put(
|
||||||
|
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||||
|
parameters={
|
||||||
|
"project_id": "Project UUID",
|
||||||
|
"node_id": "Node UUID",
|
||||||
|
"adapter_number": "Network adapter where the nio is located",
|
||||||
|
"port_number": "Port from where the nio should be updated"
|
||||||
|
},
|
||||||
|
status_codes={
|
||||||
|
201: "NIO updated",
|
||||||
|
400: "Invalid request",
|
||||||
|
404: "Instance doesn't exist"
|
||||||
|
},
|
||||||
|
input=NIO_SCHEMA,
|
||||||
|
output=NIO_SCHEMA,
|
||||||
|
description="Update a NIO from a VPCS instance")
|
||||||
|
def update_nio(request, response):
|
||||||
|
|
||||||
|
vpcs_manager = VPCS.instance()
|
||||||
|
vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||||
|
nio = vm.ethernet_adapter.get_nio(int(request.match_info["port_number"]))
|
||||||
|
if "filters" in request.json and nio:
|
||||||
|
nio.filters = request.json["filters"]
|
||||||
|
yield from vm.port_update_nio_binding(int(request.match_info["port_number"]), nio)
|
||||||
|
response.set_status(201)
|
||||||
|
response.json(request.json)
|
||||||
|
|
||||||
@Route.delete(
|
@Route.delete(
|
||||||
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
r"/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||||
parameters={
|
parameters={
|
||||||
|
@ -62,6 +62,7 @@ class LinkHandler:
|
|||||||
|
|
||||||
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
||||||
link = yield from project.add_link()
|
link = yield from project.add_link()
|
||||||
|
yield from link.update_filters(request.json.get("filters", {}))
|
||||||
try:
|
try:
|
||||||
for node in request.json["nodes"]:
|
for node in request.json["nodes"]:
|
||||||
yield from link.add_node(project.get_node(node["node_id"]),
|
yield from link.add_node(project.get_node(node["node_id"]),
|
||||||
@ -74,6 +75,24 @@ class LinkHandler:
|
|||||||
response.set_status(201)
|
response.set_status(201)
|
||||||
response.json(link)
|
response.json(link)
|
||||||
|
|
||||||
|
@Route.get(
|
||||||
|
r"/projects/{project_id}/links/{link_id}/available_filters",
|
||||||
|
parameters={
|
||||||
|
"project_id": "Project UUID",
|
||||||
|
"link_id": "Link UUID"
|
||||||
|
},
|
||||||
|
status_codes={
|
||||||
|
200: "List of filters",
|
||||||
|
400: "Invalid request"
|
||||||
|
},
|
||||||
|
description="Return the list of filters available for this link")
|
||||||
|
def list_filters(request, response):
|
||||||
|
|
||||||
|
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
||||||
|
link = project.get_link(request.match_info["link_id"])
|
||||||
|
response.set_status(200)
|
||||||
|
response.json(link.available_filters())
|
||||||
|
|
||||||
@Route.put(
|
@Route.put(
|
||||||
r"/projects/{project_id}/links/{link_id}",
|
r"/projects/{project_id}/links/{link_id}",
|
||||||
parameters={
|
parameters={
|
||||||
@ -91,7 +110,9 @@ class LinkHandler:
|
|||||||
|
|
||||||
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
|
||||||
link = project.get_link(request.match_info["link_id"])
|
link = project.get_link(request.match_info["link_id"])
|
||||||
yield from link.update_nodes(request.json["nodes"])
|
yield from link.update_filters(request.json.get("filters", {}))
|
||||||
|
if "nodes" in request.json:
|
||||||
|
yield from link.update_nodes(request.json["nodes"])
|
||||||
response.set_status(201)
|
response.set_status(201)
|
||||||
response.json(link)
|
response.json(link)
|
||||||
|
|
||||||
|
@ -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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from .label import LABEL_OBJECT_SCHEMA
|
from .label import LABEL_OBJECT_SCHEMA
|
||||||
|
from .filter import FILTER_OBJECT_SCHEMA
|
||||||
|
|
||||||
LINK_OBJECT_SCHEMA = {
|
LINK_OBJECT_SCHEMA = {
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
@ -64,6 +64,7 @@ LINK_OBJECT_SCHEMA = {
|
|||||||
"additionalProperties": False
|
"additionalProperties": False
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"filters": FILTER_OBJECT_SCHEMA,
|
||||||
"capturing": {
|
"capturing": {
|
||||||
"description": "Read only property. True if a capture running on the link",
|
"description": "Read only property. True if a capture running on the link",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@ -81,7 +82,6 @@ LINK_OBJECT_SCHEMA = {
|
|||||||
"enum": ["ethernet", "serial"]
|
"enum": ["ethernet", "serial"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["nodes"],
|
|
||||||
"additionalProperties": False
|
"additionalProperties": False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from .filter import FILTER_OBJECT_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
NIO_SCHEMA = {
|
NIO_SCHEMA = {
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
@ -43,7 +45,8 @@ NIO_SCHEMA = {
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 65535
|
"maximum": 65535
|
||||||
}
|
},
|
||||||
|
"filters": FILTER_OBJECT_SCHEMA
|
||||||
},
|
},
|
||||||
"required": ["type", "lport", "rhost", "rport"],
|
"required": ["type", "lport", "rhost", "rport"],
|
||||||
"additionalProperties": False
|
"additionalProperties": False
|
||||||
|
@ -138,8 +138,8 @@ class Hypervisor(UBridgeHypervisor):
|
|||||||
match = re.search("ubridge version ([0-9a-z\.]+)", output)
|
match = re.search("ubridge version ([0-9a-z\.]+)", output)
|
||||||
if match:
|
if match:
|
||||||
self._version = match.group(1)
|
self._version = match.group(1)
|
||||||
if parse_version(self._version) < parse_version("0.9.7"):
|
if parse_version(self._version) < parse_version("0.9.12"):
|
||||||
raise UbridgeError("uBridge executable version must be >= 0.9.7")
|
raise UbridgeError("uBridge executable version must be >= 0.9.12")
|
||||||
else:
|
else:
|
||||||
raise UbridgeError("Could not determine uBridge version for {}".format(self._path))
|
raise UbridgeError("Could not determine uBridge version for {}".format(self._path))
|
||||||
except (OSError, subprocess.SubprocessError) as e:
|
except (OSError, subprocess.SubprocessError) as e:
|
||||||
|
@ -26,7 +26,7 @@ from tests.utils import asyncio_patch
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def nio():
|
def nio():
|
||||||
return NIOUDP(4242, "127.0.0.1", 4343)
|
return NIOUDP(4242, "127.0.0.1", 4343, [])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -24,9 +24,9 @@ def test_arp_command(async_run):
|
|||||||
node = AsyncioMagicMock()
|
node = AsyncioMagicMock()
|
||||||
node.name = "Test"
|
node.name = "Test"
|
||||||
node.nios = {}
|
node.nios = {}
|
||||||
node.nios[0] = NIOUDP(55, "127.0.0.1", 56)
|
node.nios[0] = NIOUDP(55, "127.0.0.1", 56, [])
|
||||||
node.nios[0].name = "Ethernet0"
|
node.nios[0].name = "Ethernet0"
|
||||||
node.nios[1] = NIOUDP(55, "127.0.0.1", 56)
|
node.nios[1] = NIOUDP(55, "127.0.0.1", 56, [])
|
||||||
node.nios[1].name = "Ethernet1"
|
node.nios[1].name = "Ethernet1"
|
||||||
node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"])
|
node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"])
|
||||||
console = EthernetSwitchConsole(node)
|
console = EthernetSwitchConsole(node)
|
||||||
|
@ -19,7 +19,7 @@ import pytest
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from tests.utils import asyncio_patch
|
from tests.utils import asyncio_patch, AsyncioMagicMock
|
||||||
|
|
||||||
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
@ -28,6 +28,7 @@ from gns3server.compute.docker.docker_vm import DockerVM
|
|||||||
from gns3server.compute.vpcs.vpcs_error import VPCSError
|
from gns3server.compute.vpcs.vpcs_error import VPCSError
|
||||||
from gns3server.compute.error import NodeError
|
from gns3server.compute.error import NodeError
|
||||||
from gns3server.compute.vpcs import VPCS
|
from gns3server.compute.vpcs import VPCS
|
||||||
|
from gns3server.compute.nios.nio_udp import NIOUDP
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -121,3 +122,26 @@ def test_change_aux_port(node, port_manager):
|
|||||||
node.aux = port2
|
node.aux = port2
|
||||||
assert node.aux == port2
|
assert node.aux == port2
|
||||||
port_manager.reserve_tcp_port(port1, node.project)
|
port_manager.reserve_tcp_port(port1, node.project)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_ubridge_udp_connection(node, async_run):
|
||||||
|
filters = [{
|
||||||
|
"type": "latency",
|
||||||
|
"value": 10
|
||||||
|
}]
|
||||||
|
|
||||||
|
snio = NIOUDP(1245, "localhost", 1246, [])
|
||||||
|
dnio = NIOUDP(1245, "localhost", 1244, filters)
|
||||||
|
with asyncio_patch("gns3server.compute.base_node.BaseNode._ubridge_apply_filters") as mock:
|
||||||
|
async_run(node._update_ubridge_udp_connection('VPCS-10', snio, dnio))
|
||||||
|
mock.assert_called_with("VPCS-10", filters)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ubridge_apply_filters(node, async_run):
|
||||||
|
filters = {
|
||||||
|
"latency": [10]
|
||||||
|
}
|
||||||
|
node._ubridge_send = AsyncioMagicMock()
|
||||||
|
async_run(node._ubridge_apply_filters("VPCS-10", filters))
|
||||||
|
node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10")
|
||||||
|
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 latency 10")
|
||||||
|
@ -230,6 +230,7 @@ def test_json(async_run, project, compute, link):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"filters": {},
|
||||||
"link_type": "ethernet",
|
"link_type": "ethernet",
|
||||||
"capturing": False,
|
"capturing": False,
|
||||||
"capture_file_name": None,
|
"capture_file_name": None,
|
||||||
@ -262,7 +263,8 @@ def test_json(async_run, project, compute, link):
|
|||||||
'style': 'font-size: 10; font-style: Verdana'
|
'style': 'font-size: 10; font-style: Verdana'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"filters": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -348,3 +350,49 @@ def test_delete(async_run, project, compute):
|
|||||||
|
|
||||||
async_run(link.delete())
|
async_run(link.delete())
|
||||||
assert link not in node2.link
|
assert link not in node2.link
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_filters(async_run, project, compute):
|
||||||
|
node1 = Node(project, compute, "node1", node_type="qemu")
|
||||||
|
node1._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
link.create = AsyncioMagicMock()
|
||||||
|
link._project.controller.notification.emit = MagicMock()
|
||||||
|
project.dump = AsyncioMagicMock()
|
||||||
|
async_run(link.add_node(node1, 0, 4))
|
||||||
|
|
||||||
|
node2 = Node(project, compute, "node2", node_type="qemu")
|
||||||
|
node2._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||||
|
async_run(link.add_node(node2, 0, 4))
|
||||||
|
|
||||||
|
link.update = AsyncioMagicMock()
|
||||||
|
assert link._created
|
||||||
|
async_run(link.update_filters({
|
||||||
|
"packet_loss": ["10"],
|
||||||
|
"delay": ["50", "10"],
|
||||||
|
"frequency_drop": ["0"]
|
||||||
|
}))
|
||||||
|
assert link.filters == {
|
||||||
|
"packet_loss": [10],
|
||||||
|
"delay": [50, 10]
|
||||||
|
}
|
||||||
|
assert link.update.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_filters(async_run, project, compute):
|
||||||
|
node1 = Node(project, compute, "node1", node_type="qemu")
|
||||||
|
node1._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
link.create = AsyncioMagicMock()
|
||||||
|
assert link.available_filters() == []
|
||||||
|
|
||||||
|
# Qemu is not supported should return 0 filters
|
||||||
|
async_run(link.add_node(node1, 0, 4))
|
||||||
|
assert link.available_filters() == []
|
||||||
|
|
||||||
|
node2 = Node(project, compute, "node2", node_type="vpcs")
|
||||||
|
node2._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||||
|
async_run(link.add_node(node2, 0, 4))
|
||||||
|
assert len(link.available_filters()) > 0
|
||||||
|
@ -19,7 +19,7 @@ import pytest
|
|||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from tests.utils import asyncio_patch, AsyncioMagicMock
|
from tests.utils import AsyncioMagicMock
|
||||||
|
|
||||||
from gns3server.controller.project import Project
|
from gns3server.controller.project import Project
|
||||||
from gns3server.controller.udp_link import UDPLink
|
from gns3server.controller.udp_link import UDPLink
|
||||||
@ -52,6 +52,7 @@ def test_create(async_run, project):
|
|||||||
|
|
||||||
link = UDPLink(project)
|
link = UDPLink(project)
|
||||||
async_run(link.add_node(node1, 0, 4))
|
async_run(link.add_node(node1, 0, 4))
|
||||||
|
async_run(link.update_filters({"latency": [10]}))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def compute1_callback(path, data={}, **kwargs):
|
def compute1_callback(path, data={}, **kwargs):
|
||||||
@ -83,13 +84,15 @@ def test_create(async_run, project):
|
|||||||
"lport": 1024,
|
"lport": 1024,
|
||||||
"rhost": "192.168.1.2",
|
"rhost": "192.168.1.2",
|
||||||
"rport": 2048,
|
"rport": 2048,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": {"latency": [10]}
|
||||||
}, timeout=120)
|
}, timeout=120)
|
||||||
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
||||||
"lport": 2048,
|
"lport": 2048,
|
||||||
"rhost": "192.168.1.1",
|
"rhost": "192.168.1.1",
|
||||||
"rport": 1024,
|
"rport": 1024,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": {}
|
||||||
}, timeout=120)
|
}, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
@ -147,13 +150,15 @@ def test_create_one_side_failure(async_run, project):
|
|||||||
"lport": 1024,
|
"lport": 1024,
|
||||||
"rhost": "192.168.1.2",
|
"rhost": "192.168.1.2",
|
||||||
"rport": 2048,
|
"rport": 2048,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": {}
|
||||||
}, timeout=120)
|
}, timeout=120)
|
||||||
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
||||||
"lport": 2048,
|
"lport": 2048,
|
||||||
"rhost": "192.168.1.1",
|
"rhost": "192.168.1.1",
|
||||||
"rport": 1024,
|
"rport": 1024,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp",
|
||||||
|
"filters": {}
|
||||||
}, timeout=120)
|
}, timeout=120)
|
||||||
# The link creation has failed we rollback the nio
|
# The link creation has failed we rollback the nio
|
||||||
compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120)
|
compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120)
|
||||||
@ -302,3 +307,77 @@ def test_node_updated(project, async_run):
|
|||||||
node_vpcs._status = "stopped"
|
node_vpcs._status = "stopped"
|
||||||
async_run(link.node_updated(node_vpcs))
|
async_run(link.node_updated(node_vpcs))
|
||||||
assert link.stop_capture.called
|
assert link.stop_capture.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_update(async_run, project):
|
||||||
|
compute1 = MagicMock()
|
||||||
|
compute2 = MagicMock()
|
||||||
|
|
||||||
|
node1 = Node(project, compute1, "node1", node_type="vpcs")
|
||||||
|
node1._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||||
|
node2 = Node(project, compute2, "node2", node_type="vpcs")
|
||||||
|
node2._ports = [EthernetPort("E0", 0, 3, 1)]
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def subnet_callback(compute2):
|
||||||
|
"""
|
||||||
|
Fake subnet callback
|
||||||
|
"""
|
||||||
|
return ("192.168.1.1", "192.168.1.2")
|
||||||
|
|
||||||
|
compute1.get_ip_on_same_subnet.side_effect = subnet_callback
|
||||||
|
|
||||||
|
link = UDPLink(project)
|
||||||
|
async_run(link.add_node(node1, 0, 4))
|
||||||
|
async_run(link.update_filters({"latency": [10]}))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def compute1_callback(path, data={}, **kwargs):
|
||||||
|
"""
|
||||||
|
Fake server
|
||||||
|
"""
|
||||||
|
if "/ports/udp" in path:
|
||||||
|
response = MagicMock()
|
||||||
|
response.json = {"udp_port": 1024}
|
||||||
|
return response
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def compute2_callback(path, data={}, **kwargs):
|
||||||
|
"""
|
||||||
|
Fake server
|
||||||
|
"""
|
||||||
|
if "/ports/udp" in path:
|
||||||
|
response = MagicMock()
|
||||||
|
response.json = {"udp_port": 2048}
|
||||||
|
return response
|
||||||
|
|
||||||
|
compute1.post.side_effect = compute1_callback
|
||||||
|
compute1.host = "example.com"
|
||||||
|
compute2.post.side_effect = compute2_callback
|
||||||
|
compute2.host = "example.org"
|
||||||
|
async_run(link.add_node(node2, 3, 1))
|
||||||
|
|
||||||
|
compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
|
||||||
|
"lport": 1024,
|
||||||
|
"rhost": "192.168.1.2",
|
||||||
|
"rport": 2048,
|
||||||
|
"type": "nio_udp",
|
||||||
|
"filters": {"latency": [10]}
|
||||||
|
}, timeout=120)
|
||||||
|
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
||||||
|
"lport": 2048,
|
||||||
|
"rhost": "192.168.1.1",
|
||||||
|
"rport": 1024,
|
||||||
|
"type": "nio_udp",
|
||||||
|
"filters": {}
|
||||||
|
}, timeout=120)
|
||||||
|
|
||||||
|
assert link.created
|
||||||
|
async_run(link.update_filters({"drop": [5]}))
|
||||||
|
compute1.put.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
|
||||||
|
"lport": 1024,
|
||||||
|
"rhost": "192.168.1.2",
|
||||||
|
"rport": 2048,
|
||||||
|
"type": "nio_udp",
|
||||||
|
"filters": {"drop": [5]}
|
||||||
|
}, timeout=120)
|
||||||
|
@ -74,6 +74,20 @@ def test_vpcs_nio_create_udp(http_compute, vm):
|
|||||||
assert response.json["type"] == "nio_udp"
|
assert response.json["type"] == "nio_udp"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vpcs_nio_update_udp(http_compute, vm):
|
||||||
|
response = http_compute.put("/projects/{project_id}/vpcs/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]),
|
||||||
|
{
|
||||||
|
"type": "nio_udp",
|
||||||
|
"lport": 4242,
|
||||||
|
"rport": 4343,
|
||||||
|
"rhost": "127.0.0.1",
|
||||||
|
"filters": {}},
|
||||||
|
example=True)
|
||||||
|
assert response.status == 201
|
||||||
|
assert response.route == "/projects/{project_id}/vpcs/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
|
||||||
|
assert response.json["type"] == "nio_udp"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
|
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
|
||||||
def test_vpcs_nio_create_tap(http_compute, vm, ethernet_device):
|
def test_vpcs_nio_create_tap(http_compute, vm, ethernet_device):
|
||||||
with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True):
|
with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True):
|
||||||
|
@ -33,7 +33,7 @@ from gns3server.handlers.api.controller.project_handler import ProjectHandler
|
|||||||
from gns3server.controller import Controller
|
from gns3server.controller import Controller
|
||||||
from gns3server.controller.ports.ethernet_port import EthernetPort
|
from gns3server.controller.ports.ethernet_port import EthernetPort
|
||||||
from gns3server.controller.node import Node
|
from gns3server.controller.node import Node
|
||||||
from gns3server.controller.link import Link
|
from gns3server.controller.link import Link, FILTERS
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -59,6 +59,11 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
||||||
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"latency": [10],
|
||||||
|
"frequency_drop": [50]
|
||||||
|
}
|
||||||
|
|
||||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -77,7 +82,8 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
"adapter_number": 2,
|
"adapter_number": 2,
|
||||||
"port_number": 4
|
"port_number": 4
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"filters": filters
|
||||||
}, example=True)
|
}, example=True)
|
||||||
assert mock.called
|
assert mock.called
|
||||||
assert response.status == 201
|
assert response.status == 201
|
||||||
@ -85,6 +91,7 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
assert len(response.json["nodes"]) == 2
|
assert len(response.json["nodes"]) == 2
|
||||||
assert response.json["nodes"][0]["label"]["x"] == 42
|
assert response.json["nodes"][0]["label"]["x"] == 42
|
||||||
assert len(project.links) == 1
|
assert len(project.links) == 1
|
||||||
|
assert list(project.links.values())[0].filters == filters
|
||||||
|
|
||||||
|
|
||||||
def test_create_link_failure(http_controller, tmpdir, project, compute, async_run):
|
def test_create_link_failure(http_controller, tmpdir, project, compute, async_run):
|
||||||
@ -135,6 +142,11 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
||||||
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"latency": 10,
|
||||||
|
"frequency_drop": 50
|
||||||
|
}
|
||||||
|
|
||||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -174,10 +186,12 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
"adapter_number": 2,
|
"adapter_number": 2,
|
||||||
"port_number": 4
|
"port_number": 4
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"filters": filters
|
||||||
})
|
})
|
||||||
assert response.status == 201
|
assert response.status == 201
|
||||||
assert response.json["nodes"][0]["label"]["x"] == 64
|
assert response.json["nodes"][0]["label"]["x"] == 64
|
||||||
|
assert list(project.links.values())[0].filters == filters
|
||||||
|
|
||||||
|
|
||||||
def test_list_link(http_controller, tmpdir, project, compute, async_run):
|
def test_list_link(http_controller, tmpdir, project, compute, async_run):
|
||||||
@ -190,24 +204,31 @@ def test_list_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu"))
|
||||||
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
node2._ports = [EthernetPort("E0", 0, 2, 4)]
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"latency": 10,
|
||||||
|
"frequency_drop": 50
|
||||||
|
}
|
||||||
|
nodes = [
|
||||||
|
{
|
||||||
|
"node_id": node1.id,
|
||||||
|
"adapter_number": 0,
|
||||||
|
"port_number": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_id": node2.id,
|
||||||
|
"adapter_number": 2,
|
||||||
|
"port_number": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
|
||||||
response = http_controller.post("/projects/{}/links".format(project.id), {
|
response = http_controller.post("/projects/{}/links".format(project.id), {
|
||||||
"nodes": [
|
"nodes": nodes,
|
||||||
{
|
"filters": filters
|
||||||
"node_id": node1.id,
|
|
||||||
"adapter_number": 0,
|
|
||||||
"port_number": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_id": node2.id,
|
|
||||||
"adapter_number": 2,
|
|
||||||
"port_number": 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
response = http_controller.get("/projects/{}/links".format(project.id), example=True)
|
response = http_controller.get("/projects/{}/links".format(project.id), example=True)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert len(response.json) == 1
|
assert len(response.json) == 1
|
||||||
|
assert response.json[0]["filters"] == filters
|
||||||
|
|
||||||
|
|
||||||
def test_start_capture(http_controller, tmpdir, project, compute, async_run):
|
def test_start_capture(http_controller, tmpdir, project, compute, async_run):
|
||||||
@ -258,3 +279,14 @@ def test_delete_link(http_controller, tmpdir, project, compute, async_run):
|
|||||||
response = http_controller.delete("/projects/{}/links/{}".format(project.id, link.id), example=True)
|
response = http_controller.delete("/projects/{}/links/{}".format(project.id, link.id), example=True)
|
||||||
assert mock.called
|
assert mock.called
|
||||||
assert response.status == 204
|
assert response.status == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filters(http_controller, tmpdir, project, async_run):
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
with patch("gns3server.controller.link.Link.available_filters", return_value=FILTERS) as mock:
|
||||||
|
response = http_controller.get("/projects/{}/links/{}/available_filters".format(project.id, link.id), example=True)
|
||||||
|
assert mock.called
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.json == FILTERS
|
||||||
|
Loading…
Reference in New Issue
Block a user