mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-06-18 23:38:17 +00:00
Smart choice of host for UDP link
This commit is contained in:
@ -15,6 +15,7 @@
|
|||||||
# 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/>.
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
@ -96,6 +97,9 @@ class Compute:
|
|||||||
# Websocket for notifications
|
# Websocket for notifications
|
||||||
self._ws = None
|
self._ws = None
|
||||||
|
|
||||||
|
# Cache of interfaces on remote host
|
||||||
|
self._interfaces_cache = None
|
||||||
|
|
||||||
def _session(self):
|
def _session(self):
|
||||||
if self._http_session is None or self._http_session.closed is True:
|
if self._http_session is None or self._http_session.closed is True:
|
||||||
self._http_session = aiohttp.ClientSession()
|
self._http_session = aiohttp.ClientSession()
|
||||||
@ -122,6 +126,16 @@ class Compute:
|
|||||||
self._password = None
|
self._password = None
|
||||||
self._auth = aiohttp.BasicAuth(self._user, "")
|
self._auth = aiohttp.BasicAuth(self._user, "")
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def interfaces(self):
|
||||||
|
"""
|
||||||
|
Get the list of network on compute
|
||||||
|
"""
|
||||||
|
if not self._interfaces_cache:
|
||||||
|
response = yield from self.get("/network/interfaces")
|
||||||
|
self._interfaces_cache = response.json
|
||||||
|
return self._interfaces_cache
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
for kw in kwargs:
|
for kw in kwargs:
|
||||||
@ -192,6 +206,13 @@ class Compute:
|
|||||||
"""
|
"""
|
||||||
return self._host
|
return self._host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_ip(self):
|
||||||
|
"""
|
||||||
|
Return the IP associated to the host
|
||||||
|
"""
|
||||||
|
return socket.gethostbyname(self._host)
|
||||||
|
|
||||||
@host.setter
|
@host.setter
|
||||||
def host(self, host):
|
def host(self, host):
|
||||||
self._host = host
|
self._host = host
|
||||||
@ -491,3 +512,41 @@ class Compute:
|
|||||||
path = "/projects/{}/files".format(project.id)
|
path = "/projects/{}/files".format(project.id)
|
||||||
res = yield from self.http_query("GET", path, timeout=120)
|
res = yield from self.http_query("GET", path, timeout=120)
|
||||||
return res.json
|
return res.json
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def get_ip_on_same_subnet(self, other_compute):
|
||||||
|
"""
|
||||||
|
Try to found the best ip for communication from one compute
|
||||||
|
to another
|
||||||
|
|
||||||
|
:returns: Tuple (ip_for_this_compute, ip_for_other_compute)
|
||||||
|
"""
|
||||||
|
if other_compute == self:
|
||||||
|
return (self.host_ip, self.host_ip)
|
||||||
|
|
||||||
|
this_compute_interfaces = yield from self.interfaces()
|
||||||
|
other_compute_interfaces = yield from other_compute.interfaces()
|
||||||
|
|
||||||
|
# Sort interface to put the compute host in first position
|
||||||
|
# we guess that if user specified this host it could have a reason (VMware Nat / Host only interface)
|
||||||
|
this_compute_interfaces = sorted(this_compute_interfaces, key=lambda i: i["ip_address"] != self.host_ip)
|
||||||
|
other_compute_interfaces = sorted(other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip)
|
||||||
|
|
||||||
|
for this_interface in this_compute_interfaces:
|
||||||
|
if len(this_interface["ip_address"]) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
this_network = ipaddress.ip_network("{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False)
|
||||||
|
|
||||||
|
for other_interface in other_compute_interfaces:
|
||||||
|
if len(other_interface["ip_address"]) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Avoid stuff like 127.0.0.1
|
||||||
|
if other_interface["ip_address"] == this_interface["ip_address"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
other_network = ipaddress.ip_network("{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False)
|
||||||
|
if this_network.overlaps(other_network):
|
||||||
|
return (this_interface["ip_address"], other_interface["ip_address"])
|
||||||
|
raise ValueError("No common subnet for compute {} and {}".format(self.name, other_compute.name))
|
||||||
|
@ -41,7 +41,13 @@ class UDPLink(Link):
|
|||||||
adapter_number2 = self._nodes[1]["adapter_number"]
|
adapter_number2 = self._nodes[1]["adapter_number"]
|
||||||
port_number2 = self._nodes[1]["port_number"]
|
port_number2 = self._nodes[1]["port_number"]
|
||||||
|
|
||||||
# Reserve a UDP port on both side
|
# Get an IP allowing communication between both host
|
||||||
|
try:
|
||||||
|
(node1_host, node2_host) = yield from node1.compute.get_ip_on_same_subnet(node2.compute)
|
||||||
|
except ValueError as e:
|
||||||
|
raise aiohttp.web.HTTPConflict(text=str(e))
|
||||||
|
|
||||||
|
# Reserve a UDP port on both side
|
||||||
response = yield from node1.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
response = yield from node1.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
||||||
self._node1_port = response.json["udp_port"]
|
self._node1_port = response.json["udp_port"]
|
||||||
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))
|
||||||
@ -50,7 +56,7 @@ class UDPLink(Link):
|
|||||||
# Create the tunnel on both side
|
# Create the tunnel on both side
|
||||||
data = {
|
data = {
|
||||||
"lport": self._node1_port,
|
"lport": self._node1_port,
|
||||||
"rhost": node2.compute.host,
|
"rhost": node2_host,
|
||||||
"rport": self._node2_port,
|
"rport": self._node2_port,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp"
|
||||||
}
|
}
|
||||||
@ -58,7 +64,7 @@ class UDPLink(Link):
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
"lport": self._node2_port,
|
"lport": self._node2_port,
|
||||||
"rhost": node1.compute.host,
|
"rhost": node1_host,
|
||||||
"rport": self._node1_port,
|
"rport": self._node1_port,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp"
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,11 @@ def test_init(compute):
|
|||||||
assert compute.id == "my_compute_id"
|
assert compute.id == "my_compute_id"
|
||||||
|
|
||||||
|
|
||||||
|
def test_host_ip(controller):
|
||||||
|
compute = Compute("my_compute_id", protocol="https", host="localhost", port=84, controller=controller)
|
||||||
|
assert compute.host_ip == "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
def test_name():
|
def test_name():
|
||||||
c = Compute("my_compute_id", protocol="https", host="example.com", port=84, controller=MagicMock(), name=None)
|
c = Compute("my_compute_id", protocol="https", host="example.com", port=84, controller=MagicMock(), name=None)
|
||||||
assert c.name == "https://example.com:84"
|
assert c.name == "https://example.com:84"
|
||||||
@ -323,3 +328,88 @@ def test_list_files(project, async_run, compute):
|
|||||||
with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock:
|
with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock:
|
||||||
assert async_run(compute.list_files(project)) == res
|
assert async_run(compute.list_files(project)) == res
|
||||||
mock.assert_any_call("GET", "https://example.com:84/v2/compute/projects/{}/files".format(project.id), auth=None, chunked=False, data=None, headers={'content-type': 'application/json'})
|
mock.assert_any_call("GET", "https://example.com:84/v2/compute/projects/{}/files".format(project.id), auth=None, chunked=False, data=None, headers={'content-type': 'application/json'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_interfaces(project, async_run, compute):
|
||||||
|
res = [
|
||||||
|
{
|
||||||
|
"id": "vmnet99",
|
||||||
|
"ip_address": "172.16.97.1",
|
||||||
|
"mac_address": "00:50:56:c0:00:63",
|
||||||
|
"name": "vmnet99",
|
||||||
|
"netmask": "255.255.255.0",
|
||||||
|
"type": "ethernet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
response = AsyncioMagicMock()
|
||||||
|
response.read = AsyncioMagicMock(return_value=json.dumps(res).encode())
|
||||||
|
response.status = 200
|
||||||
|
with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock:
|
||||||
|
assert async_run(compute.interfaces()) == res
|
||||||
|
mock.assert_any_call("GET", "https://example.com:84/v2/compute/network/interfaces", auth=None, chunked=False, data=None, headers={'content-type': 'application/json'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ip_on_same_subnet(controller, async_run):
|
||||||
|
compute1 = Compute("compute1", host="192.168.1.1", controller=controller)
|
||||||
|
compute1._interfaces_cache = [
|
||||||
|
{
|
||||||
|
"ip_address": "127.0.0.1",
|
||||||
|
"netmask": "255.255.255.255"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.2.1",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.1",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Case 1 both host are on the same network
|
||||||
|
compute2 = Compute("compute2", host="192.168.1.2", controller=controller)
|
||||||
|
compute2._interfaces_cache = [
|
||||||
|
{
|
||||||
|
"ip_address": "127.0.0.1",
|
||||||
|
"netmask": "255.255.255.255"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.2.2",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.2",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert async_run(compute1.get_ip_on_same_subnet(compute2)) == ("192.168.1.1", "192.168.1.2")
|
||||||
|
|
||||||
|
# Case 2 compute2 host is on a different network but a common interface is available
|
||||||
|
compute2 = Compute("compute2", host="192.168.4.2", controller=controller)
|
||||||
|
compute2._interfaces_cache = [
|
||||||
|
{
|
||||||
|
"ip_address": "127.0.0.1",
|
||||||
|
"netmask": "255.255.255.255"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.4.2",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.2",
|
||||||
|
"netmask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert async_run(compute1.get_ip_on_same_subnet(compute2)) == ("192.168.1.1", "192.168.1.2")
|
||||||
|
|
||||||
|
#No common interface
|
||||||
|
# Case 2 compute2 host is on a different network but a common interface is available
|
||||||
|
compute2 = Compute("compute2", host="127.0.0.1", controller=controller)
|
||||||
|
compute2._interfaces_cache = [
|
||||||
|
{
|
||||||
|
"ip_address": "127.0.0.1",
|
||||||
|
"netmask": "255.255.255.255"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
async_run(compute1.get_ip_on_same_subnet(compute2))
|
||||||
|
@ -38,6 +38,15 @@ def test_create(async_run, project):
|
|||||||
node1 = Node(project, compute1, "node1", node_type="vpcs")
|
node1 = Node(project, compute1, "node1", node_type="vpcs")
|
||||||
node2 = Node(project, compute2, "node2", node_type="vpcs")
|
node2 = Node(project, compute2, "node2", node_type="vpcs")
|
||||||
|
|
||||||
|
@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)
|
link = UDPLink(project)
|
||||||
async_run(link.add_node(node1, 0, 4))
|
async_run(link.add_node(node1, 0, 4))
|
||||||
async_run(link.add_node(node2, 3, 1))
|
async_run(link.add_node(node2, 3, 1))
|
||||||
@ -70,13 +79,13 @@ def test_create(async_run, project):
|
|||||||
|
|
||||||
compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
|
compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={
|
||||||
"lport": 1024,
|
"lport": 1024,
|
||||||
"rhost": compute2.host,
|
"rhost": "192.168.1.2",
|
||||||
"rport": 2048,
|
"rport": 2048,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp"
|
||||||
})
|
})
|
||||||
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": compute1.host,
|
"rhost": "192.168.1.1",
|
||||||
"rport": 1024,
|
"rport": 1024,
|
||||||
"type": "nio_udp"
|
"type": "nio_udp"
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user