mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 11:18:11 +00:00
Smart choice of host for UDP link
This commit is contained in:
parent
c01bb2436e
commit
134fed8fc5
@ -15,6 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ipaddress
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import socket
|
||||
@ -96,6 +97,9 @@ class Compute:
|
||||
# Websocket for notifications
|
||||
self._ws = None
|
||||
|
||||
# Cache of interfaces on remote host
|
||||
self._interfaces_cache = None
|
||||
|
||||
def _session(self):
|
||||
if self._http_session is None or self._http_session.closed is True:
|
||||
self._http_session = aiohttp.ClientSession()
|
||||
@ -122,6 +126,16 @@ class Compute:
|
||||
self._password = None
|
||||
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
|
||||
def update(self, **kwargs):
|
||||
for kw in kwargs:
|
||||
@ -192,6 +206,13 @@ class Compute:
|
||||
"""
|
||||
return self._host
|
||||
|
||||
@property
|
||||
def host_ip(self):
|
||||
"""
|
||||
Return the IP associated to the host
|
||||
"""
|
||||
return socket.gethostbyname(self._host)
|
||||
|
||||
@host.setter
|
||||
def host(self, host):
|
||||
self._host = host
|
||||
@ -491,3 +512,41 @@ class Compute:
|
||||
path = "/projects/{}/files".format(project.id)
|
||||
res = yield from self.http_query("GET", path, timeout=120)
|
||||
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"]
|
||||
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))
|
||||
self._node1_port = response.json["udp_port"]
|
||||
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
|
||||
data = {
|
||||
"lport": self._node1_port,
|
||||
"rhost": node2.compute.host,
|
||||
"rhost": node2_host,
|
||||
"rport": self._node2_port,
|
||||
"type": "nio_udp"
|
||||
}
|
||||
@ -58,7 +64,7 @@ class UDPLink(Link):
|
||||
|
||||
data = {
|
||||
"lport": self._node2_port,
|
||||
"rhost": node1.compute.host,
|
||||
"rhost": node1_host,
|
||||
"rport": self._node1_port,
|
||||
"type": "nio_udp"
|
||||
}
|
||||
|
@ -40,6 +40,11 @@ def test_init(compute):
|
||||
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():
|
||||
c = Compute("my_compute_id", protocol="https", host="example.com", port=84, controller=MagicMock(), name=None)
|
||||
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:
|
||||
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'})
|
||||
|
||||
|
||||
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")
|
||||
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)
|
||||
async_run(link.add_node(node1, 0, 4))
|
||||
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={
|
||||
"lport": 1024,
|
||||
"rhost": compute2.host,
|
||||
"rhost": "192.168.1.2",
|
||||
"rport": 2048,
|
||||
"type": "nio_udp"
|
||||
})
|
||||
compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={
|
||||
"lport": 2048,
|
||||
"rhost": compute1.host,
|
||||
"rhost": "192.168.1.1",
|
||||
"rport": 1024,
|
||||
"type": "nio_udp"
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user