Skip to content

Commit

Permalink
Replace by another TCP port if port is already used
Browse files Browse the repository at this point in the history
Another version of #370

This time we replace a free TCP port if port is used and raise
a warning to the user.
  • Loading branch information
julien-duponchelle committed Dec 7, 2015
1 parent f3b71dc commit 2aaad47
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 20 deletions.
7 changes: 5 additions & 2 deletions gns3server/modules/base_vm.py
Expand Up @@ -60,11 +60,14 @@ def __init__(self, name, vm_id, project, manager, console=None, console_type="te
self._vm_status = "stopped"

if self._console is not None:
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project)
if console_type == "vnc":
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project, port_range_start=5900, port_range_end=6000)
else:
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project)
else:
if console_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._console = self._manager.port_manager.get_free_tcp_port(self._project, 5900, 6000)
self._console = self._manager.port_manager.get_free_tcp_port(self._project, port_range_start=5900, port_range_end=6000)
else:
self._console = self._manager.port_manager.get_free_tcp_port(self._project)

Expand Down
70 changes: 56 additions & 14 deletions gns3server/modules/port_manager.py
Expand Up @@ -142,22 +142,16 @@ def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP",
if end_port < start_port:
raise HTTPConflict(text="Invalid port range {}-{}".format(start_port, end_port))

if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
else:
socket_type = socket.SOCK_STREAM

last_exception = None
for port in range(start_port, end_port + 1):
if port in ignore_ports:
continue

last_exception
try:
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket_type, 0, socket.AI_PASSIVE):
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(sa) # the port is available if bind is a success
return port
PortManager._check_port(host, port, socket_type)
return port
except OSError as e:
last_exception = e
if port + 1 == end_port:
Expand All @@ -169,6 +163,25 @@ def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP",
end_port,
host,
last_exception))
@staticmethod
def _check_port(host, port, socket_type):
"""
Check if an a port is available and raise an OSError if port is not available
:returns: boolean
"""
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
else:
socket_type = socket.SOCK_STREAM

for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket_type, 0, socket.AI_PASSIVE):
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(sa) # the port is available if bind is a success
return True


def get_free_tcp_port(self, project, port_range_start=None, port_range_end=None):
"""
Expand All @@ -193,18 +206,47 @@ def get_free_tcp_port(self, project, port_range_start=None, port_range_end=None)
log.debug("TCP port {} has been allocated".format(port))
return port

def reserve_tcp_port(self, port, project):
def reserve_tcp_port(self, port, project, port_range_start=None, port_range_end=None):
"""
Reserve a specific TCP port number
Reserve a specific TCP port number. If not available replace it
by another.
:param port: TCP port number
:param project: Project instance
:param port_range_start: Port range to use
:param port_range_end: Port range to use
:returns: The TCP port
"""

# use the default range is not specific one is given
if port_range_start is None and port_range_end is None:
port_range_start = self._console_port_range[0]
port_range_end = self._console_port_range[1]

if port in self._used_tcp_ports:
raise HTTPConflict(text="TCP port {} already in use on host".format(port, self._console_host))
old_port = port
port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end)
msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port)
log.warning(msg)
project.emit("log.warning", {"message": msg})
return port
if port < self._console_port_range[0] or port > self._console_port_range[1]:
raise HTTPConflict(text="TCP port {} is outside the range {}-{}".format(port, self._console_port_range[0], self._console_port_range[1]))
old_port = port
port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end)
msg = "TCP port {} is outside the range {}-{} on host {}. Port has been replaced by {}".format(old_port, port_range_start, port_range_end, self._console_host, port)
log.warning(msg)
project.emit("log.warning", {"message": msg})
return port
try:
PortManager._check_port(self._console_host, port, "TCP")
except OSError:
old_port = port
port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end)
msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port)
log.warning(msg)
project.emit("log.warning", {"message": msg})
return port

self._used_tcp_ports.add(port)
project.record_tcp_port(port)
log.debug("TCP port {} has been reserved".format(port))
Expand Down
59 changes: 55 additions & 4 deletions tests/modules/test_port_manager.py
Expand Up @@ -27,15 +27,66 @@ def test_reserve_tcp_port():
pm = PortManager()
project = Project()
pm.reserve_tcp_port(2001, project)
with pytest.raises(aiohttp.web.HTTPConflict):
pm.reserve_tcp_port(2001, project)
with patch("gns3server.modules.project.Project.emit") as mock_emit:
port = pm.reserve_tcp_port(2001, project)
assert port != 2001
assert mock_emit.call_args[0][0] == "log.warning"


def test_reserve_tcp_port_outside_range():
pm = PortManager()
project = Project()
with pytest.raises(aiohttp.web.HTTPConflict):
pm.reserve_tcp_port(80, project)
with patch("gns3server.modules.project.Project.emit") as mock_emit:
port = pm.reserve_tcp_port(80, project)
assert port != 80
assert mock_emit.call_args[0][0] == "log.warning"


def test_reserve_tcp_port_already_used():
"""
This test simulate a scenario where the port is already taken
by another programm on the server
"""

pm = PortManager()
project = Project()
with patch("gns3server.modules.port_manager.PortManager._check_port") as mock_check:

def execute_mock(host, port, *args):
if port == 2001:
raise OSError("Port is already used")
else:
return True

mock_check.side_effect = execute_mock

with patch("gns3server.modules.project.Project.emit") as mock_emit:
port = pm.reserve_tcp_port(2001, project)
assert port != 2001
assert mock_emit.call_args[0][0] == "log.warning"

def test_reserve_tcp_port_already_used():
"""
This test simulate a scenario where the port is already taken
by another programm on the server
"""

pm = PortManager()
project = Project()
with patch("gns3server.modules.port_manager.PortManager._check_port") as mock_check:

def execute_mock(host, port, *args):
if port == 2001:
raise OSError("Port is already used")
else:
return True

mock_check.side_effect = execute_mock

with patch("gns3server.modules.project.Project.emit") as mock_emit:
port = pm.reserve_tcp_port(2001, project)
assert port != 2001
assert mock_emit.call_args[0][0] == "log.warning"


def test_reserve_udp_port():
Expand Down

0 comments on commit 2aaad47

Please sign in to comment.