1
0
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:
Julien Duponchelle 2016-08-25 19:14:29 +02:00
parent c01bb2436e
commit 134fed8fc5
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
4 changed files with 169 additions and 5 deletions

View File

@ -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))

View File

@ -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"
}

View File

@ -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))

View File

@ -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"
})