mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-24 17:28:08 +00:00
Dirty stop start for VPCS
This commit is contained in:
parent
482fdf9031
commit
6c35cc304e
@ -19,16 +19,13 @@ from ..web.route import Route
|
|||||||
from ..schemas.vpcs import VPCS_CREATE_SCHEMA
|
from ..schemas.vpcs import VPCS_CREATE_SCHEMA
|
||||||
from ..schemas.vpcs import VPCS_OBJECT_SCHEMA
|
from ..schemas.vpcs import VPCS_OBJECT_SCHEMA
|
||||||
from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA
|
from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA
|
||||||
from ..modules import VPCS
|
from ..modules.vpcs import VPCS
|
||||||
|
|
||||||
|
|
||||||
class VPCSHandler(object):
|
class VPCSHandler(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
@Route.post(
|
@Route.post(
|
||||||
r"/vpcs",
|
r"/vpcs",
|
||||||
parameters={
|
|
||||||
"vpcs_id": "Id of VPCS instance"
|
|
||||||
},
|
|
||||||
status_codes={
|
status_codes={
|
||||||
201: "Success of creation of VPCS",
|
201: "Success of creation of VPCS",
|
||||||
409: "Conflict"
|
409: "Conflict"
|
||||||
@ -43,6 +40,38 @@ class VPCSHandler(object):
|
|||||||
"vpcs_id": vm.id,
|
"vpcs_id": vm.id,
|
||||||
"console": 4242})
|
"console": 4242})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@Route.post(
|
||||||
|
r"/vpcs/{vpcs_id}/start",
|
||||||
|
parameters={
|
||||||
|
"vpcs_id": "Id of VPCS instance"
|
||||||
|
},
|
||||||
|
status_codes={
|
||||||
|
201: "Success of creation of VPCS",
|
||||||
|
},
|
||||||
|
description="Start VPCS",
|
||||||
|
)
|
||||||
|
def create(request, response):
|
||||||
|
vpcs_manager = VPCS.instance()
|
||||||
|
vm = yield from vpcs_manager.start_vm(int(request.match_info['vpcs_id']))
|
||||||
|
response.json({})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@Route.post(
|
||||||
|
r"/vpcs/{vpcs_id}/stop",
|
||||||
|
parameters={
|
||||||
|
"vpcs_id": "Id of VPCS instance"
|
||||||
|
},
|
||||||
|
status_codes={
|
||||||
|
201: "Success of stopping VPCS",
|
||||||
|
},
|
||||||
|
description="Stop VPCS",
|
||||||
|
)
|
||||||
|
def create(request, response):
|
||||||
|
vpcs_manager = VPCS.instance()
|
||||||
|
vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id']))
|
||||||
|
response.json({})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@Route.get(
|
@Route.get(
|
||||||
r"/vpcs/{vpcs_id}",
|
r"/vpcs/{vpcs_id}",
|
||||||
|
@ -19,12 +19,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from .vm_error import VMError
|
from .device_error import DeviceError
|
||||||
|
|
||||||
|
|
||||||
class VMManager:
|
class BaseManager:
|
||||||
"""
|
"""
|
||||||
Base class for all VMManager.
|
Base class for all Manager.
|
||||||
Responsible of management of a VM pool
|
Responsible of management of a VM pool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -69,10 +69,10 @@ class VMManager:
|
|||||||
identifier = i
|
identifier = i
|
||||||
break
|
break
|
||||||
if identifier == 0:
|
if identifier == 0:
|
||||||
raise VMError("Maximum number of VM instances reached")
|
raise DeviceError("Maximum number of VM instances reached")
|
||||||
else:
|
else:
|
||||||
if identifier in self._vms:
|
if identifier in self._vms:
|
||||||
raise VMError("VM identifier {} is already used by another VM instance".format(identifier))
|
raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier))
|
||||||
vm = self._VM_CLASS(vmname, identifier)
|
vm = self._VM_CLASS(vmname, identifier)
|
||||||
yield from vm.wait_for_creation()
|
yield from vm.wait_for_creation()
|
||||||
self._vms[vm.id] = vm
|
self._vms[vm.id] = vm
|
@ -17,18 +17,74 @@
|
|||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from .vm_error import VMError
|
from .device_error import DeviceError
|
||||||
|
from .attic import find_unused_port
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BaseVM:
|
class BaseVM:
|
||||||
|
_allocated_console_ports = []
|
||||||
|
|
||||||
def __init__(self, name, identifier):
|
def __init__(self, name, identifier):
|
||||||
|
self._loop = asyncio.get_event_loop()
|
||||||
|
self._allocate_console()
|
||||||
self._queue = asyncio.Queue()
|
self._queue = asyncio.Queue()
|
||||||
self._name = name
|
self._name = name
|
||||||
self._id = identifier
|
self._id = identifier
|
||||||
self._created = asyncio.Future()
|
self._created = asyncio.Future()
|
||||||
self._worker = asyncio.async(self._run())
|
self._worker = asyncio.async(self._run())
|
||||||
|
log.info("{type} device {name} [id={id}] has been created".format(
|
||||||
|
type=self.__class__.__name__,
|
||||||
|
name=self._name,
|
||||||
|
id=self._id))
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_console(self):
|
||||||
|
if not self._console:
|
||||||
|
# allocate a console port
|
||||||
|
try:
|
||||||
|
self._console = find_unused_port(self._console_start_port_range,
|
||||||
|
self._console_end_port_range,
|
||||||
|
self._console_host,
|
||||||
|
ignore_ports=self._allocated_console_ports)
|
||||||
|
except Exception as e:
|
||||||
|
raise DeviceError(e)
|
||||||
|
|
||||||
|
if self._console in self._allocated_console_ports:
|
||||||
|
raise DeviceError("Console port {} is already used by another device".format(console))
|
||||||
|
self._allocated_console_ports.append(self._console)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def console(self):
|
||||||
|
"""
|
||||||
|
Returns the TCP console port.
|
||||||
|
|
||||||
|
:returns: console port (integer)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._console
|
||||||
|
|
||||||
|
@console.setter
|
||||||
|
def console(self, console):
|
||||||
|
"""
|
||||||
|
Sets the TCP console port.
|
||||||
|
|
||||||
|
:param console: console port (integer)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if console in self._allocated_console_ports:
|
||||||
|
raise VPCSError("Console port {} is already used by another VPCS device".format(console))
|
||||||
|
|
||||||
|
self._allocated_console_ports.remove(self._console)
|
||||||
|
self._console = console
|
||||||
|
self._allocated_console_ports.append(self._console)
|
||||||
|
log.info("{type} {name} [id={id}]: console port set to {port}".format(
|
||||||
|
type=self.__class__.__name__,
|
||||||
|
name=self._name,
|
||||||
|
id=self._id,
|
||||||
|
port=console))
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
"""
|
"""
|
||||||
@ -65,7 +121,7 @@ class BaseVM:
|
|||||||
try:
|
try:
|
||||||
yield from self._create()
|
yield from self._create()
|
||||||
self._created.set_result(True)
|
self._created.set_result(True)
|
||||||
except VMError as e:
|
except DeviceError as e:
|
||||||
self._created.set_exception(e)
|
self._created.set_exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -75,7 +131,7 @@ class BaseVM:
|
|||||||
try:
|
try:
|
||||||
yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout)
|
yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout))
|
raise DeviceError("{} has timed out after {} seconds!".format(subcommand, timeout))
|
||||||
future.set_result(True)
|
future.set_result(True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
future.set_exception(e)
|
future.set_exception(e)
|
||||||
@ -83,6 +139,15 @@ class BaseVM:
|
|||||||
def wait_for_creation(self):
|
def wait_for_creation(self):
|
||||||
return self._created
|
return self._created
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def start():
|
||||||
|
"""
|
||||||
|
Starts the VM process.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def put(self, *args):
|
def put(self, *args):
|
||||||
"""
|
"""
|
||||||
Add to the processing queue of the VM
|
Add to the processing queue of the VM
|
||||||
@ -95,5 +160,5 @@ class BaseVM:
|
|||||||
args.insert(0, future)
|
args.insert(0, future)
|
||||||
self._queue.put_nowait(args)
|
self._queue.put_nowait(args)
|
||||||
except asyncio.qeues.QueueFull:
|
except asyncio.qeues.QueueFull:
|
||||||
raise VMError("Queue is full")
|
raise DeviceError("Queue is full")
|
||||||
return future
|
return future
|
||||||
|
@ -16,5 +16,5 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
class VMError(Exception):
|
class DeviceError(Exception):
|
||||||
pass
|
pass
|
@ -19,12 +19,9 @@
|
|||||||
VPCS server module.
|
VPCS server module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ..vm_manager import VMManager
|
from ..base_manager import BaseManager
|
||||||
from .vpcs_device import VPCSDevice
|
from .vpcs_device import VPCSDevice
|
||||||
|
|
||||||
|
|
||||||
class VPCS(VMManager):
|
class VPCS(BaseManager):
|
||||||
_VM_CLASS = VPCSDevice
|
_VM_CLASS = VPCSDevice
|
||||||
|
|
||||||
def create_vm(self, name):
|
|
||||||
return super().create_vm(name)
|
|
||||||
|
0
gns3server/modules/vpcs/adapters/__init__.py
Normal file
0
gns3server/modules/vpcs/adapters/__init__.py
Normal file
104
gns3server/modules/vpcs/adapters/adapter.py
Normal file
104
gns3server/modules/vpcs/adapters/adapter.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
class Adapter(object):
|
||||||
|
"""
|
||||||
|
Base class for adapters.
|
||||||
|
|
||||||
|
:param interfaces: number of interfaces supported by this adapter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, interfaces=1):
|
||||||
|
|
||||||
|
self._interfaces = interfaces
|
||||||
|
|
||||||
|
self._ports = {}
|
||||||
|
for port_id in range(0, interfaces):
|
||||||
|
self._ports[port_id] = None
|
||||||
|
|
||||||
|
def removable(self):
|
||||||
|
"""
|
||||||
|
Returns True if the adapter can be removed from a slot
|
||||||
|
and False if not.
|
||||||
|
|
||||||
|
:returns: boolean
|
||||||
|
"""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def port_exists(self, port_id):
|
||||||
|
"""
|
||||||
|
Checks if a port exists on this adapter.
|
||||||
|
|
||||||
|
:returns: True is the port exists,
|
||||||
|
False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if port_id in self._ports:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_nio(self, port_id, nio):
|
||||||
|
"""
|
||||||
|
Adds a NIO to a port on this adapter.
|
||||||
|
|
||||||
|
:param port_id: port ID (integer)
|
||||||
|
:param nio: NIO instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._ports[port_id] = nio
|
||||||
|
|
||||||
|
def remove_nio(self, port_id):
|
||||||
|
"""
|
||||||
|
Removes a NIO from a port on this adapter.
|
||||||
|
|
||||||
|
:param port_id: port ID (integer)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._ports[port_id] = None
|
||||||
|
|
||||||
|
def get_nio(self, port_id):
|
||||||
|
"""
|
||||||
|
Returns the NIO assigned to a port.
|
||||||
|
|
||||||
|
:params port_id: port ID (integer)
|
||||||
|
|
||||||
|
:returns: NIO instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._ports[port_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ports(self):
|
||||||
|
"""
|
||||||
|
Returns port to NIO mapping
|
||||||
|
|
||||||
|
:returns: dictionary port -> NIO
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._ports
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interfaces(self):
|
||||||
|
"""
|
||||||
|
Returns the number of interfaces supported by this adapter.
|
||||||
|
|
||||||
|
:returns: number of interfaces
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._interfaces
|
31
gns3server/modules/vpcs/adapters/ethernet_adapter.py
Normal file
31
gns3server/modules/vpcs/adapters/ethernet_adapter.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014 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/>.
|
||||||
|
|
||||||
|
from .adapter import Adapter
|
||||||
|
|
||||||
|
|
||||||
|
class EthernetAdapter(Adapter):
|
||||||
|
"""
|
||||||
|
VPCS Ethernet adapter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Adapter.__init__(self, interfaces=1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return "VPCS Ethernet adapter"
|
0
gns3server/modules/vpcs/nios/__init__.py
Normal file
0
gns3server/modules/vpcs/nios/__init__.py
Normal file
46
gns3server/modules/vpcs/nios/nio_tap.py
Normal file
46
gns3server/modules/vpcs/nios/nio_tap.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 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/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interface for TAP NIOs (UNIX based OSes only).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class NIO_TAP(object):
|
||||||
|
"""
|
||||||
|
TAP NIO.
|
||||||
|
|
||||||
|
:param tap_device: TAP device name (e.g. tap0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tap_device):
|
||||||
|
|
||||||
|
self._tap_device = tap_device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tap_device(self):
|
||||||
|
"""
|
||||||
|
Returns the TAP device used by this NIO.
|
||||||
|
|
||||||
|
:returns: the TAP device name
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._tap_device
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return "NIO TAP"
|
72
gns3server/modules/vpcs/nios/nio_udp.py
Normal file
72
gns3server/modules/vpcs/nios/nio_udp.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 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/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interface for UDP NIOs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class NIO_UDP(object):
|
||||||
|
"""
|
||||||
|
UDP NIO.
|
||||||
|
|
||||||
|
:param lport: local port number
|
||||||
|
:param rhost: remote address/host
|
||||||
|
:param rport: remote port number
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance_count = 0
|
||||||
|
|
||||||
|
def __init__(self, lport, rhost, rport):
|
||||||
|
|
||||||
|
self._lport = lport
|
||||||
|
self._rhost = rhost
|
||||||
|
self._rport = rport
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lport(self):
|
||||||
|
"""
|
||||||
|
Returns the local port
|
||||||
|
|
||||||
|
:returns: local port number
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._lport
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rhost(self):
|
||||||
|
"""
|
||||||
|
Returns the remote host
|
||||||
|
|
||||||
|
:returns: remote address/host
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._rhost
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rport(self):
|
||||||
|
"""
|
||||||
|
Returns the remote port
|
||||||
|
|
||||||
|
:returns: remote port number
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._rport
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return "NIO UDP"
|
@ -15,9 +15,370 @@
|
|||||||
# 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/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
VPCS device management (creates command line, processes, files etc.) in
|
||||||
|
order to run an VPCS instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pkg_resources import parse_version
|
||||||
|
from .vpcs_error import VPCSError
|
||||||
|
from .adapters.ethernet_adapter import EthernetAdapter
|
||||||
|
from .nios.nio_udp import NIO_UDP
|
||||||
|
from .nios.nio_tap import NIO_TAP
|
||||||
|
|
||||||
from ..base_vm import BaseVM
|
from ..base_vm import BaseVM
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class VPCSDevice(BaseVM):
|
class VPCSDevice(BaseVM):
|
||||||
pass
|
"""
|
||||||
|
VPCS device implementation.
|
||||||
|
|
||||||
|
:param name: name of this VPCS device
|
||||||
|
:param vpcs_id: VPCS instance ID
|
||||||
|
:param path: path to VPCS executable
|
||||||
|
:param working_dir: path to a working directory
|
||||||
|
:param console: TCP console port
|
||||||
|
:param console_host: IP address to bind for console connections
|
||||||
|
:param console_start_port_range: TCP console port range start
|
||||||
|
:param console_end_port_range: TCP console port range end
|
||||||
|
"""
|
||||||
|
def __init__(self, name, vpcs_id,
|
||||||
|
path = None,
|
||||||
|
working_dir = None,
|
||||||
|
console=None,
|
||||||
|
console_host="0.0.0.0",
|
||||||
|
console_start_port_range=4512,
|
||||||
|
console_end_port_range=5000):
|
||||||
|
|
||||||
|
#self._path = path
|
||||||
|
#self._working_dir = working_dir
|
||||||
|
# TODO: Hardcodded for testing
|
||||||
|
self._path = "/usr/local/bin/vpcs"
|
||||||
|
self._working_dir = "/tmp"
|
||||||
|
|
||||||
|
self._console = console
|
||||||
|
self._console_host = console_host
|
||||||
|
self._command = []
|
||||||
|
self._process = None
|
||||||
|
self._vpcs_stdout_file = ""
|
||||||
|
self._started = False
|
||||||
|
self._console_start_port_range = console_start_port_range
|
||||||
|
self._console_end_port_range = console_end_port_range
|
||||||
|
|
||||||
|
# VPCS settings
|
||||||
|
self._script_file = ""
|
||||||
|
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
|
||||||
|
|
||||||
|
# working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
|
||||||
|
#
|
||||||
|
# if vpcs_id and not os.path.isdir(working_dir_path):
|
||||||
|
# raise VPCSError("Working directory {} doesn't exist".format(working_dir_path))
|
||||||
|
#
|
||||||
|
# # create the device own working directory
|
||||||
|
# self.working_dir = working_dir_path
|
||||||
|
#
|
||||||
|
|
||||||
|
super().__init__(name, vpcs_id)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _create(self):
|
||||||
|
"""Called when run loop is started"""
|
||||||
|
self._check_requirement()
|
||||||
|
|
||||||
|
def _check_requirement(self):
|
||||||
|
"""Check if VPCS is available with the correct version"""
|
||||||
|
if not self._path:
|
||||||
|
raise VPCSError("No path to a VPCS executable has been set")
|
||||||
|
|
||||||
|
if not os.path.isfile(self._path):
|
||||||
|
raise VPCSError("VPCS program '{}' is not accessible".format(self._path))
|
||||||
|
|
||||||
|
if not os.access(self._path, os.X_OK):
|
||||||
|
raise VPCSError("VPCS program '{}' is not executable".format(self._path))
|
||||||
|
|
||||||
|
yield from self._check_vpcs_version()
|
||||||
|
|
||||||
|
def defaults(self):
|
||||||
|
"""
|
||||||
|
Returns all the default attribute values for VPCS.
|
||||||
|
|
||||||
|
:returns: default values (dictionary)
|
||||||
|
"""
|
||||||
|
|
||||||
|
vpcs_defaults = {"name": self._name,
|
||||||
|
"script_file": self._script_file,
|
||||||
|
"console": self._console}
|
||||||
|
|
||||||
|
return vpcs_defaults
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset(cls):
|
||||||
|
"""
|
||||||
|
Resets allocated instance list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cls._instances.clear()
|
||||||
|
cls._allocated_console_ports.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""
|
||||||
|
Returns the name of this VPCS device.
|
||||||
|
|
||||||
|
:returns: name
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, new_name):
|
||||||
|
"""
|
||||||
|
Sets the name of this VPCS device.
|
||||||
|
|
||||||
|
:param new_name: name
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._script_file:
|
||||||
|
# update the startup.vpc
|
||||||
|
config_path = os.path.join(self._working_dir, "startup.vpc")
|
||||||
|
if os.path.isfile(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, "r+", errors="replace") as f:
|
||||||
|
old_config = f.read()
|
||||||
|
new_config = old_config.replace(self._name, new_name)
|
||||||
|
f.seek(0)
|
||||||
|
f.write(new_config)
|
||||||
|
except OSError as e:
|
||||||
|
raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e))
|
||||||
|
|
||||||
|
log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
|
||||||
|
id=self._id,
|
||||||
|
new_name=new_name))
|
||||||
|
self._name = new_name
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _check_vpcs_version(self):
|
||||||
|
"""
|
||||||
|
Checks if the VPCS executable version is >= 0.5b1.
|
||||||
|
"""
|
||||||
|
#TODO: should be async
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
|
||||||
|
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
|
||||||
|
if match:
|
||||||
|
version = match.group(1)
|
||||||
|
if parse_version(version) < parse_version("0.5b1"):
|
||||||
|
raise VPCSError("VPCS executable version must be >= 0.5b1")
|
||||||
|
else:
|
||||||
|
raise VPCSError("Could not determine the VPCS version for {}".format(self._path))
|
||||||
|
except (OSError, subprocess.SubprocessError) as e:
|
||||||
|
raise VPCSError("Error while looking for the VPCS version: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
Starts the VPCS process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.is_running():
|
||||||
|
# if not self._ethernet_adapter.get_nio(0):
|
||||||
|
# raise VPCSError("This VPCS instance must be connected in order to start")
|
||||||
|
|
||||||
|
self._command = self._build_command()
|
||||||
|
try:
|
||||||
|
log.info("starting VPCS: {}".format(self._command))
|
||||||
|
self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log")
|
||||||
|
log.info("logging to {}".format(self._vpcs_stdout_file))
|
||||||
|
flags = 0
|
||||||
|
if sys.platform.startswith("win32"):
|
||||||
|
flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
with open(self._vpcs_stdout_file, "w") as fd:
|
||||||
|
self._process = yield from asyncio.create_subprocess_exec(*self._command,
|
||||||
|
stdout=fd,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
cwd=self._working_dir,
|
||||||
|
creationflags=flags)
|
||||||
|
log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
|
||||||
|
self._started = True
|
||||||
|
except (OSError, subprocess.SubprocessError) as e:
|
||||||
|
vpcs_stdout = self.read_vpcs_stdout()
|
||||||
|
log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
|
||||||
|
raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stops the VPCS process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# stop the VPCS process
|
||||||
|
if self.is_running():
|
||||||
|
log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid))
|
||||||
|
if sys.platform.startswith("win32"):
|
||||||
|
self._process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||||
|
else:
|
||||||
|
self._process.terminate()
|
||||||
|
|
||||||
|
self._process.wait()
|
||||||
|
|
||||||
|
self._process = None
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
def read_vpcs_stdout(self):
|
||||||
|
"""
|
||||||
|
Reads the standard output of the VPCS process.
|
||||||
|
Only use when the process has been stopped or has crashed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = ""
|
||||||
|
if self._vpcs_stdout_file:
|
||||||
|
try:
|
||||||
|
with open(self._vpcs_stdout_file, errors="replace") as file:
|
||||||
|
output = file.read()
|
||||||
|
except OSError as e:
|
||||||
|
log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
"""
|
||||||
|
Checks if the VPCS process is running
|
||||||
|
|
||||||
|
:returns: True or False
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._process:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def port_add_nio_binding(self, port_id, nio):
|
||||||
|
"""
|
||||||
|
Adds a port NIO binding.
|
||||||
|
|
||||||
|
:param port_id: port ID
|
||||||
|
:param nio: NIO instance to add to the slot/port
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._ethernet_adapter.port_exists(port_id):
|
||||||
|
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
|
||||||
|
port_id=port_id))
|
||||||
|
|
||||||
|
self._ethernet_adapter.add_nio(port_id, nio)
|
||||||
|
log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name,
|
||||||
|
id=self._id,
|
||||||
|
nio=nio,
|
||||||
|
port_id=port_id))
|
||||||
|
|
||||||
|
def port_remove_nio_binding(self, port_id):
|
||||||
|
"""
|
||||||
|
Removes a port NIO binding.
|
||||||
|
|
||||||
|
:param port_id: port ID
|
||||||
|
|
||||||
|
:returns: NIO instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._ethernet_adapter.port_exists(port_id):
|
||||||
|
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
|
||||||
|
port_id=port_id))
|
||||||
|
|
||||||
|
nio = self._ethernet_adapter.get_nio(port_id)
|
||||||
|
self._ethernet_adapter.remove_nio(port_id)
|
||||||
|
log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name,
|
||||||
|
id=self._id,
|
||||||
|
nio=nio,
|
||||||
|
port_id=port_id))
|
||||||
|
return nio
|
||||||
|
|
||||||
|
def _build_command(self):
|
||||||
|
"""
|
||||||
|
Command to start the VPCS process.
|
||||||
|
(to be passed to subprocess.Popen())
|
||||||
|
|
||||||
|
VPCS command line:
|
||||||
|
usage: vpcs [options] [scriptfile]
|
||||||
|
Option:
|
||||||
|
-h print this help then exit
|
||||||
|
-v print version information then exit
|
||||||
|
|
||||||
|
-i num number of vpc instances to start (default is 9)
|
||||||
|
-p port run as a daemon listening on the tcp 'port'
|
||||||
|
-m num start byte of ether address, default from 0
|
||||||
|
-r file load and execute script file
|
||||||
|
compatible with older versions, DEPRECATED.
|
||||||
|
|
||||||
|
-e tap mode, using /dev/tapx by default (linux only)
|
||||||
|
-u udp mode, default
|
||||||
|
|
||||||
|
udp mode options:
|
||||||
|
-s port local udp base port, default from 20000
|
||||||
|
-c port remote udp base port (dynamips udp port), default from 30000
|
||||||
|
-t ip remote host IP, default 127.0.0.1
|
||||||
|
|
||||||
|
tap mode options:
|
||||||
|
-d device device name, works only when -i is set to 1
|
||||||
|
|
||||||
|
hypervisor mode option:
|
||||||
|
-H port run as the hypervisor listening on the tcp 'port'
|
||||||
|
|
||||||
|
If no 'scriptfile' specified, vpcs will read and execute the file named
|
||||||
|
'startup.vpc' if it exsits in the current directory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
command = [self._path]
|
||||||
|
command.extend(["-p", str(self._console)]) # listen to console port
|
||||||
|
|
||||||
|
nio = self._ethernet_adapter.get_nio(0)
|
||||||
|
if nio:
|
||||||
|
if isinstance(nio, NIO_UDP):
|
||||||
|
# UDP tunnel
|
||||||
|
command.extend(["-s", str(nio.lport)]) # source UDP port
|
||||||
|
command.extend(["-c", str(nio.rport)]) # destination UDP port
|
||||||
|
command.extend(["-t", nio.rhost]) # destination host
|
||||||
|
|
||||||
|
elif isinstance(nio, NIO_TAP):
|
||||||
|
# TAP interface
|
||||||
|
command.extend(["-e"])
|
||||||
|
command.extend(["-d", nio.tap_device])
|
||||||
|
|
||||||
|
command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
|
||||||
|
command.extend(["-i", "1"]) # option to start only one VPC instance
|
||||||
|
command.extend(["-F"]) # option to avoid the daemonization of VPCS
|
||||||
|
if self._script_file:
|
||||||
|
command.extend([self._script_file])
|
||||||
|
return command
|
||||||
|
|
||||||
|
@property
|
||||||
|
def script_file(self):
|
||||||
|
"""
|
||||||
|
Returns the script-file for this VPCS instance.
|
||||||
|
|
||||||
|
:returns: path to script-file
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._script_file
|
||||||
|
|
||||||
|
@script_file.setter
|
||||||
|
def script_file(self, script_file):
|
||||||
|
"""
|
||||||
|
Sets the script-file for this VPCS instance.
|
||||||
|
|
||||||
|
:param script_file: path to base-script-file
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._script_file = script_file
|
||||||
|
log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name,
|
||||||
|
id=self._id,
|
||||||
|
config=self._script_file))
|
||||||
|
40
gns3server/modules/vpcs/vpcs_error.py
Normal file
40
gns3server/modules/vpcs/vpcs_error.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 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/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Custom exceptions for VPCS module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..device_error import DeviceError
|
||||||
|
|
||||||
|
class VPCSError(DeviceError):
|
||||||
|
|
||||||
|
def __init__(self, message, original_exception=None):
|
||||||
|
|
||||||
|
Exception.__init__(self, message)
|
||||||
|
if isinstance(message, Exception):
|
||||||
|
message = str(message)
|
||||||
|
self._message = message
|
||||||
|
self._original_exception = original_exception
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return self._message
|
@ -20,6 +20,7 @@ import jsonschema
|
|||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
from ..modules.device_error import DeviceError
|
||||||
from .response import Response
|
from .response import Response
|
||||||
|
|
||||||
|
|
||||||
@ -97,6 +98,10 @@ class Route(object):
|
|||||||
response = Response(route=route)
|
response = Response(route=route)
|
||||||
response.set_status(e.status)
|
response.set_status(e.status)
|
||||||
response.json({"message": e.text, "status": e.status})
|
response.json({"message": e.text, "status": e.status})
|
||||||
|
except DeviceError as e:
|
||||||
|
response = Response(route=route)
|
||||||
|
response.set_status(400)
|
||||||
|
response.json({"message": str(e), "status": 400})
|
||||||
return response
|
return response
|
||||||
|
|
||||||
cls._routes.append((method, cls._path, control_schema))
|
cls._routes.append((method, cls._path, control_schema))
|
||||||
|
41
tests/modules/vpcs/test_vpcs_device.py
Normal file
41
tests/modules/vpcs/test_vpcs_device.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# -*- 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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from gns3server.modules.vpcs.vpcs_device import VPCSDevice
|
||||||
|
from gns3server.modules.vpcs.vpcs_error import VPCSError
|
||||||
|
|
||||||
|
@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8"))
|
||||||
|
def test_vm(tmpdir):
|
||||||
|
vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test")
|
||||||
|
assert vm.name == "test"
|
||||||
|
assert vm.id == 42
|
||||||
|
|
||||||
|
@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8"))
|
||||||
|
def test_vm_invalid_vpcs_version(tmpdir):
|
||||||
|
with pytest.raises(VPCSError):
|
||||||
|
vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test")
|
||||||
|
assert vm.name == "test"
|
||||||
|
assert vm.id == 42
|
||||||
|
|
||||||
|
def test_vm_invalid_vpcs_path(tmpdir):
|
||||||
|
with pytest.raises(VPCSError):
|
||||||
|
vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake")
|
||||||
|
assert vm.name == "test"
|
||||||
|
assert vm.id == 42
|
||||||
|
|
Loading…
Reference in New Issue
Block a user