2015-01-15 17:59:01 +02:00
# -*- 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/>.
2015-01-19 00:41:53 +02:00
import socket
2015-01-15 17:59:01 +02:00
import ipaddress
2015-01-20 21:09:20 +02:00
from aiohttp . web import HTTPConflict
2015-01-23 22:01:23 +02:00
from gns3server . config import Config
2015-01-16 01:50:36 +02:00
2015-01-23 22:01:23 +02:00
import logging
log = logging . getLogger ( __name__ )
2015-01-19 23:43:35 +02:00
2015-01-20 14:24:00 +02:00
2015-01-23 22:01:23 +02:00
class PortManager :
2015-01-31 23:34:49 +02:00
2015-01-15 17:59:01 +02:00
"""
2015-01-19 00:41:53 +02:00
: param host : IP address to bind for console connections
2015-01-15 17:59:01 +02:00
"""
2015-01-23 22:01:23 +02:00
def __init__ ( self , host = " 127.0.0.1 " ) :
2015-01-19 00:41:53 +02:00
self . _console_host = host
2015-07-21 04:22:20 +03:00
# UDP host must be 0.0.0.0, reason: https://github.com/GNS3/gns3-server/issues/265
self . _udp_host = " 0.0.0.0 "
2015-01-19 00:41:53 +02:00
self . _used_tcp_ports = set ( )
self . _used_udp_ports = set ( )
2015-01-15 17:59:01 +02:00
2015-01-23 22:01:23 +02:00
server_config = Config . instance ( ) . get_section_config ( " Server " )
remote_console_connections = server_config . getboolean ( " allow_remote_console " )
2015-11-09 03:14:55 +02:00
console_start_port_range = server_config . getint ( " console_start_port_range " , 2001 )
console_end_port_range = server_config . getint ( " console_end_port_range " , 7000 )
2015-02-02 05:43:55 +02:00
self . _console_port_range = ( console_start_port_range , console_end_port_range )
2015-02-02 23:52:58 +02:00
log . debug ( " Console port range is {} - {} " . format ( console_start_port_range , console_end_port_range ) )
2015-02-02 05:43:55 +02:00
udp_start_port_range = server_config . getint ( " udp_start_port_range " , 10000 )
udp_end_port_range = server_config . getint ( " udp_end_port_range " , 20000 )
self . _udp_port_range = ( udp_start_port_range , udp_end_port_range )
2015-02-02 23:52:58 +02:00
log . debug ( " UDP port range is {} - {} " . format ( udp_start_port_range , udp_end_port_range ) )
2015-02-02 05:43:55 +02:00
2015-01-23 22:01:23 +02:00
if remote_console_connections :
log . warning ( " Remote console connections are allowed " )
2015-01-19 00:41:53 +02:00
if ipaddress . ip_address ( host ) . version == 6 :
2015-01-15 17:59:01 +02:00
self . _console_host = " :: "
else :
self . _console_host = " 0.0.0.0 "
else :
2015-01-19 00:41:53 +02:00
self . _console_host = host
2015-01-25 00:32:58 +02:00
PortManager . _instance = self
@classmethod
def instance ( cls ) :
"""
Singleton to return only one instance of PortManager .
: returns : instance of PortManager
"""
if not hasattr ( cls , " _instance " ) or cls . _instance is None :
cls . _instance = cls ( )
return cls . _instance
2015-01-19 00:41:53 +02:00
@property
def console_host ( self ) :
return self . _console_host
@console_host.setter
2015-03-03 15:37:34 +02:00
def console_host ( self , new_host ) :
2015-01-19 00:41:53 +02:00
self . _console_host = new_host
@property
def console_port_range ( self ) :
return self . _console_port_range
2015-11-03 13:34:22 +02:00
@console_port_range.setter
2015-01-19 00:41:53 +02:00
def console_port_range ( self , new_range ) :
assert isinstance ( new_range , tuple )
self . _console_port_range = new_range
@property
def udp_host ( self ) :
return self . _udp_host
@udp_host.setter
def host ( self , new_host ) :
self . _udp_host = new_host
@property
def udp_port_range ( self ) :
return self . _udp_port_range
2015-11-03 13:34:22 +02:00
@udp_port_range.setter
2015-01-19 00:41:53 +02:00
def udp_port_range ( self , new_range ) :
assert isinstance ( new_range , tuple )
self . _udp_port_range = new_range
2015-02-24 02:42:55 +02:00
@property
def tcp_ports ( self ) :
return self . _used_tcp_ports
@property
def udp_ports ( self ) :
return self . _used_udp_ports
2015-01-19 00:41:53 +02:00
@staticmethod
def find_unused_port ( start_port , end_port , host = " 127.0.0.1 " , socket_type = " TCP " , ignore_ports = [ ] ) :
"""
Finds an unused port in a range .
: param start_port : first port in the range
: param end_port : last port in the range
: param host : host / address for bind ( )
: param socket_type : TCP ( default ) or UDP
: param ignore_ports : list of port to ignore within the range
"""
if end_port < start_port :
2015-05-06 11:40:51 +03:00
raise HTTPConflict ( text = " Invalid port range {} - {} " . format ( start_port , end_port ) )
2015-01-19 00:41:53 +02:00
last_exception = None
for port in range ( start_port , end_port + 1 ) :
if port in ignore_ports :
continue
2015-12-07 13:26:46 +02:00
last_exception
2015-01-19 00:41:53 +02:00
try :
2015-12-07 13:26:46 +02:00
PortManager . _check_port ( host , port , socket_type )
return port
2015-01-19 00:41:53 +02:00
except OSError as e :
last_exception = e
if port + 1 == end_port :
break
else :
continue
2015-01-21 04:02:22 +02:00
raise HTTPConflict ( text = " Could not find a free port between {} and {} on host {} , last exception: {} " . format ( start_port ,
2015-01-21 12:33:24 +02:00
end_port ,
host ,
last_exception ) )
2015-12-07 13:26:46 +02:00
@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
2015-01-19 00:41:53 +02:00
2015-07-04 01:06:25 +03:00
def get_free_tcp_port ( self , project , port_range_start = None , port_range_end = None ) :
2015-01-19 00:41:53 +02:00
"""
2015-02-24 02:42:55 +02:00
Get an available TCP port and reserve it
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-01-19 00:41:53 +02:00
"""
2015-07-04 01:06:25 +03:00
# 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 ]
port = self . find_unused_port ( port_range_start ,
port_range_end ,
2015-01-19 00:41:53 +02:00
host = self . _console_host ,
socket_type = " TCP " ,
ignore_ports = self . _used_tcp_ports )
self . _used_tcp_ports . add ( port )
2015-03-22 01:19:12 +02:00
project . record_tcp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " TCP port {} has been allocated " . format ( port ) )
2015-01-15 17:59:01 +02:00
return port
2015-12-07 13:26:46 +02:00
def reserve_tcp_port ( self , port , project , port_range_start = None , port_range_end = None ) :
2015-01-19 00:41:53 +02:00
"""
2015-12-07 13:26:46 +02:00
Reserve a specific TCP port number . If not available replace it
by another .
2015-01-19 00:41:53 +02:00
: param port : TCP port number
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-12-07 13:26:46 +02:00
: param port_range_start : Port range to use
: param port_range_end : Port range to use
: returns : The TCP port
2015-01-19 00:41:53 +02:00
"""
2015-12-07 13:26:46 +02:00
# 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 ]
2015-01-19 00:41:53 +02:00
if port in self . _used_tcp_ports :
2015-12-07 13:26:46 +02:00
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
2015-10-30 16:15:28 +02:00
if port < self . _console_port_range [ 0 ] or port > self . _console_port_range [ 1 ] :
2015-12-07 13:26:46 +02:00
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
2015-01-19 00:41:53 +02:00
self . _used_tcp_ports . add ( port )
2015-03-22 01:19:12 +02:00
project . record_tcp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " TCP port {} has been reserved " . format ( port ) )
2015-01-20 21:54:46 +02:00
return port
2015-01-19 00:41:53 +02:00
2015-03-22 01:19:12 +02:00
def release_tcp_port ( self , port , project ) :
2015-01-19 00:41:53 +02:00
"""
2015-02-24 02:42:55 +02:00
Release a specific TCP port number
2015-01-19 00:41:53 +02:00
: param port : TCP port number
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-01-19 00:41:53 +02:00
"""
2015-01-24 01:38:46 +02:00
if port in self . _used_tcp_ports :
self . _used_tcp_ports . remove ( port )
2015-03-22 01:19:12 +02:00
project . remove_tcp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " TCP port {} has been released " . format ( port ) )
2015-01-19 00:41:53 +02:00
2015-03-22 01:19:12 +02:00
def get_free_udp_port ( self , project ) :
2015-01-15 17:59:01 +02:00
"""
2015-01-19 00:41:53 +02:00
Get an available UDP port and reserve it
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-01-19 00:41:53 +02:00
"""
port = self . find_unused_port ( self . _udp_port_range [ 0 ] ,
self . _udp_port_range [ 1 ] ,
host = self . _udp_host ,
socket_type = " UDP " ,
ignore_ports = self . _used_udp_ports )
self . _used_udp_ports . add ( port )
2015-03-22 01:19:12 +02:00
project . record_udp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " UDP port {} has been allocated " . format ( port ) )
2015-01-19 00:41:53 +02:00
return port
2015-01-15 17:59:01 +02:00
2015-03-22 01:19:12 +02:00
def reserve_udp_port ( self , port , project ) :
2015-01-15 17:59:01 +02:00
"""
2015-01-19 00:41:53 +02:00
Reserve a specific UDP port number
2015-01-15 17:59:01 +02:00
2015-01-19 00:41:53 +02:00
: param port : UDP port number
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-01-15 17:59:01 +02:00
"""
2015-01-19 00:41:53 +02:00
if port in self . _used_udp_ports :
2015-02-10 03:24:13 +02:00
raise HTTPConflict ( text = " UDP port {} already in use on host " . format ( port , self . _console_host ) )
2015-10-30 16:15:28 +02:00
if port < self . _udp_port_range [ 0 ] or port > self . _udp_port_range [ 1 ] :
raise HTTPConflict ( text = " UDP port {} is outside the range {} - {} " . format ( port , self . _udp_port_range [ 0 ] , self . _udp_port_range [ 1 ] ) )
2015-01-19 00:41:53 +02:00
self . _used_udp_ports . add ( port )
2015-03-22 01:19:12 +02:00
project . record_udp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " UDP port {} has been reserved " . format ( port ) )
2015-01-19 00:41:53 +02:00
2015-04-15 16:58:31 +03:00
def release_udp_port ( self , port , project ) :
2015-01-15 17:59:01 +02:00
"""
2015-01-19 00:41:53 +02:00
Release a specific UDP port number
2015-01-16 01:50:36 +02:00
2015-01-19 00:41:53 +02:00
: param port : UDP port number
2015-03-22 01:19:12 +02:00
: param project : Project instance
2015-01-19 00:41:53 +02:00
"""
2015-01-15 17:59:01 +02:00
2015-01-24 01:38:46 +02:00
if port in self . _used_udp_ports :
self . _used_udp_ports . remove ( port )
2015-03-22 01:19:12 +02:00
project . remove_udp_port ( port )
2015-02-24 02:42:55 +02:00
log . debug ( " UDP port {} has been released " . format ( port ) )