Windows support (freezing).

Client notification support.
Hypervisor manager changes.
IOU reload support.
Switch to non-dynamic module loading because of a multiprocessing
problem on Windows.
This commit is contained in:
grossmj 2014-03-15 21:41:04 -06:00
parent 3df5cdb76f
commit 41a1d16e92
15 changed files with 192 additions and 45 deletions

View File

@ -18,6 +18,7 @@
import datetime import datetime
import sys import sys
import multiprocessing
import logging import logging
import tornado.options import tornado.options
import gns3server import gns3server
@ -34,6 +35,10 @@ def main():
Entry point for GNS3 server Entry point for GNS3 server
""" """
if sys.platform.startswith("win"):
# necessary on Windows to use freezing software
multiprocessing.freeze_support()
current_year = datetime.date.today().year current_year = datetime.date.today().year
print("GNS3 server version {}".format(gns3server.__version__)) print("GNS3 server version {}".format(gns3server.__version__))
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))

View File

@ -15,4 +15,13 @@
# 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 sys
from .base import IModule from .base import IModule
from .dynamips import Dynamips
from .iou import IOU
MODULES = [Dynamips]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
MODULES.append(IOU)

View File

@ -19,6 +19,7 @@
Base class (interface) for modules Base class (interface) for modules
""" """
import sys
import gns3server.jsonrpc as jsonrpc import gns3server.jsonrpc as jsonrpc
import multiprocessing import multiprocessing
import zmq import zmq
@ -109,7 +110,10 @@ class IModule(multiprocessing.Process):
log.warning("Module {} got signal {}, exiting...".format(self.name, signum)) log.warning("Module {} got signal {}, exiting...".format(self.name, signum))
self.stop() self.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]: signals = [signal.SIGTERM, signal.SIGINT]
if not sys.platform.startswith("win"):
signals.extend([signal.SIGHUP, signal.SIGQUIT])
for sig in signals:
signal.signal(sig, signal_handler) signal.signal(sig, signal_handler)
log.info("{} module running with PID {}".format(self.name, self.pid)) log.info("{} module running with PID {}".format(self.name, self.pid))
@ -179,6 +183,20 @@ class IModule(multiprocessing.Process):
self._current_call_id)) self._current_call_id))
self._stream.send_json(response) self._stream.send_json(response)
def send_notification(self, results):
"""
Sends a notification
:param results: JSON results to the ZeroMQ server
"""
jsonrpc_response = jsonrpc.JSONRPCNotification(results)()
# add session to the response
response = [self._current_session, jsonrpc_response]
log.debug("ZeroMQ client ({}) sending: {}".format(self.name, response))
self._stream.send_json(response)
def _decode_request(self, request): def _decode_request(self, request):
""" """
Decodes the request to JSON. Decodes the request to JSON.

View File

@ -102,6 +102,7 @@ class Dynamips(IModule):
IModule.__init__(self, name, *args, **kwargs) IModule.__init__(self, name, *args, **kwargs)
self._hypervisor_manager = None self._hypervisor_manager = None
self._hypervisor_manager_settings = {}
self._remote_server = False self._remote_server = False
self._routers = {} self._routers = {}
self._ethernet_switches = {} self._ethernet_switches = {}
@ -110,6 +111,8 @@ class Dynamips(IModule):
self._ethernet_hubs = {} self._ethernet_hubs = {}
self._projects_dir = kwargs["projects_dir"] self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"] self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._dynamips = ""
#self._callback = self.add_periodic_callback(self.test, 1000) #self._callback = self.add_periodic_callback(self.test, 1000)
#self._callback.start() #self._callback.start()
@ -161,6 +164,34 @@ class Dynamips(IModule):
self._remote_server = False self._remote_server = False
log.info("dynamips module has been reset") log.info("dynamips module has been reset")
def start_hypervisor_manager(self):
"""
Starts the hypervisor manager.
"""
# check if Dynamips path exists
if not os.path.exists(self._dynamips):
raise DynamipsError("Dynamips executable {} doesn't exist".format(self._dynamips))
# check if Dynamips is executable
if not os.access(self._dynamips, os.X_OK):
raise DynamipsError("Dynamips {} is not executable".format(self._dynamips))
# check if the working directory exists
if not os.path.exists(self._working_dir):
raise DynamipsError("Working directory {} doesn't exist".format(self._working_dir))
# check if the working directory is writable
if not os.access(self._working_dir, os.W_OK):
raise DynamipsError("Cannot write to working directory {}".format(self._working_dir))
log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(self._working_dir))
self._hypervisor_manager = HypervisorManager(self._dynamips, self._working_dir)
for name, value in self._hypervisor_manager_settings.items():
if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value:
setattr(self._hypervisor_manager, name, value)
@IModule.route("dynamips.settings") @IModule.route("dynamips.settings")
def settings(self, request): def settings(self, request):
""" """
@ -184,31 +215,23 @@ class Dynamips(IModule):
#TODO: JSON schema validation #TODO: JSON schema validation
# starts the hypervisor manager if it hasn't been started yet # starts the hypervisor manager if it hasn't been started yet
if not self._hypervisor_manager: if not self._hypervisor_manager:
dynamips_path = request["path"] self._dynamips = request.pop("path")
if "working_dir" in request: if "working_dir" in request:
working_dir = request["working_dir"] self._working_dir = request.pop("working_dir")
log.info("this server is local") log.info("this server is local")
else: else:
self._remote_server = True self._remote_server = True
log.info("this server is remote") log.info("this server is remote")
working_dir = self._projects_dir self._working_dir = self._projects_dir
#TODO: check if executable self._hypervisor_manager_settings = request
if not os.path.exists(dynamips_path):
raise DynamipsError("Dynamips executable {} doesn't exist".format(working_dir))
#TODO: check if writable else:
if not os.path.exists(working_dir): # apply settings to the hypervisor manager
raise DynamipsError("Working directory {} doesn't exist".format(working_dir)) for name, value in request.items():
if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value:
log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(working_dir)) setattr(self._hypervisor_manager, name, value)
self._hypervisor_manager = HypervisorManager(dynamips_path, working_dir)
# apply settings to the hypervisor manager
for name, value in request.items():
if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value:
setattr(self._hypervisor_manager, name, value)
@IModule.route("dynamips.echo") @IModule.route("dynamips.echo")
def echo(self, request): def echo(self, request):

View File

@ -47,6 +47,9 @@ class ATMSW(object):
name = request["name"] name = request["name"]
try: try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device()
atmsw = ATMSwitch(hypervisor, name) atmsw = ATMSwitch(hypervisor, name)
except DynamipsError as e: except DynamipsError as e:

View File

@ -46,6 +46,9 @@ class ETHHUB(object):
name = request["name"] name = request["name"]
try: try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device()
ethhub = Hub(hypervisor, name) ethhub = Hub(hypervisor, name)
except DynamipsError as e: except DynamipsError as e:

View File

@ -46,6 +46,9 @@ class ETHSW(object):
name = request["name"] name = request["name"]
try: try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device()
ethsw = EthernetSwitch(hypervisor, name) ethsw = EthernetSwitch(hypervisor, name)
except DynamipsError as e: except DynamipsError as e:

View File

@ -46,6 +46,9 @@ class FRSW(object):
name = request["name"] name = request["name"]
try: try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device() hypervisor = self._hypervisor_manager.allocate_hypervisor_for_simulated_device()
frsw = FrameRelaySwitch(hypervisor, name) frsw = FrameRelaySwitch(hypervisor, name)
except DynamipsError as e: except DynamipsError as e:

View File

@ -127,7 +127,7 @@ class VM(object):
try: try:
if not self._hypervisor_manager: if not self._hypervisor_manager:
raise DynamipsError("Dynamips manager is not started") self.start_hypervisor_manager()
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram) hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram)

View File

@ -67,9 +67,9 @@ class C7200(Router):
# first slot is a mandatory Input/Output controller (based on NPE type) # first slot is a mandatory Input/Output controller (based on NPE type)
if npe == "npe-g2": if npe == "npe-g2":
self._slots[0] = C7200_IO_GE_E() self.slot_add_binding(0, C7200_IO_GE_E())
else: else:
self._slots[0] = C7200_IO_2FE() self.slot_add_binding(0, C7200_IO_2FE())
def defaults(self): def defaults(self):
""" """

View File

@ -1205,7 +1205,8 @@ class Router(object):
slot_id=slot_id)) slot_id=slot_id))
if adapter == None: if adapter == None:
return raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name,
slot_id=slot_id))
#FIXME: check if adapter can be removed! #FIXME: check if adapter can be removed!

View File

@ -45,9 +45,6 @@ class IOU(IModule):
def __init__(self, name, *args, **kwargs): def __init__(self, name, *args, **kwargs):
if not sys.platform.startswith("linux"):
raise IOUError("Sorry the IOU module only works on Linux")
# get the iouyap location # get the iouyap location
config = Config.instance() config = Config.instance()
iou_config = config.get_section_config(name.upper()) iou_config = config.get_section_config(name.upper())
@ -74,14 +71,14 @@ class IOU(IModule):
self._udp_start_port_range = 30001 self._udp_start_port_range = 30001
self._udp_end_port_range = 40001 self._udp_end_port_range = 40001
self._current_udp_port = self._udp_start_port_range self._current_udp_port = self._udp_start_port_range
self._host = "127.0.0.1" #FIXME: used by ZeroMQ... self._host = "127.0.0.1" # FIXME: used by ZeroMQ...
self._projects_dir = kwargs["projects_dir"] self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"] self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir self._working_dir = self._projects_dir
self._iourc = "" self._iourc = ""
#self._callback = self.add_periodic_callback(self.test, 1000) self._iou_callback = self.add_periodic_callback(self._check_iou, 5000)
#self._callback.start() self._iou_callback.start()
def stop(self): def stop(self):
""" """
@ -95,6 +92,17 @@ class IOU(IModule):
IModule.stop(self) # this will stop the I/O loop IModule.stop(self) # this will stop the I/O loop
def _check_iou(self):
for iou_id in self._iou_instances:
iou_instance = self._iou_instances[iou_id]
if iou_instance.started and not iou_instance.is_running():
self.send_notification({"module": self.name,
"id": iou_id,
"name": iou_instance.name,
"message": "IOU is not running"})
iou_instance.stop()
@IModule.route("iou.reset") @IModule.route("iou.reset")
def reset(self, request): def reset(self, request):
""" """
@ -115,6 +123,13 @@ class IOU(IModule):
self._remote_server = False self._remote_server = False
self._current_console_port = self._console_start_port_range self._current_console_port = self._console_start_port_range
self._current_udp_port = self._udp_start_port_range self._current_udp_port = self._udp_start_port_range
if self._iourc and os.path.exists(self._iourc):
try:
os.remove(self._iourc)
except EnvironmentError as e:
log.warn("could not delete iourc file {}: {}".format(self._iourc, e))
log.info("IOU module has been reset") log.info("IOU module has been reset")
@IModule.route("iou.settings") @IModule.route("iou.settings")
@ -360,6 +375,37 @@ class IOU(IModule):
return return
self.send_response(request) self.send_response(request)
@IModule.route("iou.reload")
def vm_reload(self, request):
"""
Reloads an IOU instance.
Mandatory request parameters:
- id (IOU identifier)
Response parameters:
- same as original request
:param request: JSON request
"""
if request == None:
self.send_param_error()
return
#TODO: JSON schema validation for the request
log.debug("received request {}".format(request))
iou_id = request["id"]
iou_instance = self._iou_instances[iou_id]
try:
if iou_instance.is_running():
iou_instance.stop()
iou_instance.start()
except IOUError as e:
self.send_custom_error(str(e))
return
self.send_response(request)
@IModule.route("iou.allocate_udp_port") @IModule.route("iou.allocate_udp_port")
def allocate_udp_port(self, request): def allocate_udp_port(self, request):
""" """

View File

@ -79,6 +79,7 @@ class IOUDevice(object):
self._ioucon_thead = None self._ioucon_thead = None
self._ioucon_thread_stop_event = None self._ioucon_thread_stop_event = None
self._host = host self._host = host
self._started = False
# IOU settings # IOU settings
self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces
@ -295,6 +296,16 @@ class IOUDevice(object):
log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name, log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
@property
def started(self):
"""
Returns either this IOU device has been started or not.
:returns: boolean
"""
return self._started
def _update_iouyap_config(self): def _update_iouyap_config(self):
""" """
Updates the iouyap.ini file. Updates the iouyap.ini file.
@ -411,6 +422,7 @@ class IOUDevice(object):
cwd=self._working_dir, cwd=self._working_dir,
env=env) env=env)
log.info("IOU instance {} started PID={}".format(self._id, self._process.pid)) log.info("IOU instance {} started PID={}".format(self._id, self._process.pid))
self._started = True
except EnvironmentError as e: except EnvironmentError as e:
log.error("could not start IOU: {}".format(e)) log.error("could not start IOU: {}".format(e))
raise IOUError("could not start IOU: {}".format(e)) raise IOUError("could not start IOU: {}".format(e))
@ -437,6 +449,7 @@ class IOUDevice(object):
log.warn("IOU instance {} PID={} is still running".format(self._id, log.warn("IOU instance {} PID={} is still running".format(self._id,
self._process.pid)) self._process.pid))
self._process = None self._process = None
self._started = False
# stop console support # stop console support
if self._ioucon_thead: if self._ioucon_thead:

View File

@ -23,6 +23,7 @@ import zmq
from zmq.eventloop import ioloop, zmqstream from zmq.eventloop import ioloop, zmqstream
ioloop.install() ioloop.install()
import sys
import os import os
import tempfile import tempfile
import signal import signal
@ -37,6 +38,7 @@ from .handlers.jsonrpc_websocket import JSONRPCWebSocket
from .handlers.version_handler import VersionHandler from .handlers.version_handler import VersionHandler
from .handlers.file_upload_handler import FileUploadHandler from .handlers.file_upload_handler import FileUploadHandler
from .module_manager import ModuleManager from .module_manager import ModuleManager
from .modules import MODULES
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -85,23 +87,38 @@ class Server(object):
Loads the modules. Loads the modules.
""" """
cwd = os.path.dirname(os.path.abspath(__file__)) #=======================================================================
module_path = os.path.join(cwd, 'modules') # cwd = os.path.dirname(os.path.abspath(__file__))
log.info("loading modules from {}".format(module_path)) # module_path = os.path.join(cwd, 'modules')
module_manager = ModuleManager([module_path]) # log.info("loading modules from {}".format(module_path))
module_manager.load_modules() # module_manager = ModuleManager([module_path])
for module in module_manager.get_all_modules(): # module_manager.load_modules()
instance = module_manager.activate_module(module, # for module in module_manager.get_all_modules():
"127.0.0.1", # ZeroMQ server address # instance = module_manager.activate_module(module,
self._zmq_port, # ZeroMQ server port # "127.0.0.1", # ZeroMQ server address
projects_dir=self._projects_dir, # self._zmq_port, # ZeroMQ server port
temp_dir=self._temp_dir) # projects_dir=self._projects_dir,
if not instance: # temp_dir=self._temp_dir)
continue # if not instance:
# continue
# self._modules.append(instance)
# destinations = instance.destinations()
# for destination in destinations:
# JSONRPCWebSocket.register_destination(destination, module.name)
# instance.start() # starts the new process
#=======================================================================
for module in MODULES:
instance = module(module.__name__.lower(),
"127.0.0.1", # ZeroMQ server address
self._zmq_port, # ZeroMQ server port
projects_dir=self._projects_dir,
temp_dir=self._temp_dir)
self._modules.append(instance) self._modules.append(instance)
destinations = instance.destinations() destinations = instance.destinations()
for destination in destinations: for destination in destinations:
JSONRPCWebSocket.register_destination(destination, module.name) JSONRPCWebSocket.register_destination(destination, instance.name)
instance.start() # starts the new process instance.start() # starts the new process
def run(self): def run(self):
@ -130,7 +147,10 @@ class Server(object):
log.warning("Server got signal {}, exiting...".format(signum)) log.warning("Server got signal {}, exiting...".format(signum))
self._cleanup() self._cleanup()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]: signals = [signal.SIGTERM, signal.SIGINT]
if not sys.platform.startswith("win"):
signals.extend([signal.SIGHUP, signal.SIGQUIT])
for sig in signals:
signal.signal(sig, signal_handler) signal.signal(sig, signal_handler)
try: try:

View File

@ -23,5 +23,5 @@
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
__version__ = "0.1.dev" __version__ = "0.9.dev"
__version_info__ = (0, 1, 0, -99) __version_info__ = (0, 9, 0, -99)