gns3-server/gns3server/modules/vpcs/vpcs_vm.py

420 lines
15 KiB
Python
Raw Normal View History

2015-01-14 01:26:32 +00: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-14 17:52:02 +00:00
"""
VPCS VM management (creates command line, processes, files etc.) in
2015-01-14 17:52:02 +00:00
order to run an VPCS instance.
"""
import os
import sys
import subprocess
import signal
import re
import asyncio
import shutil
2015-01-14 17:52:02 +00:00
from pkg_resources import parse_version
from .vpcs_error import VPCSError
2015-01-18 23:26:56 +00:00
from ..adapters.ethernet_adapter import EthernetAdapter
from ..nios.nio_udp import NIOUDP
from ..nios.nio_tap import NIOTAP
2015-01-14 01:26:32 +00:00
from ..base_vm import BaseVM
2015-02-16 16:20:07 +00:00
from ...utils.asyncio import subprocess_check_output
2015-01-14 01:26:32 +00:00
2015-01-14 17:52:02 +00:00
import logging
log = logging.getLogger(__name__)
2015-01-14 01:26:32 +00:00
2015-01-20 12:12:26 +00:00
class VPCSVM(BaseVM):
2015-01-21 10:33:24 +00:00
module_name = 'vpcs'
2015-01-20 12:24:00 +00:00
2015-01-14 17:52:02 +00:00
"""
2015-01-20 12:12:26 +00:00
VPCS vm implementation.
2015-01-14 17:52:02 +00:00
2015-02-11 04:50:02 +00:00
:param name: The name of this VM
:param vm_id: VPCS instance identifier
2015-01-20 11:46:15 +00:00
:param project: Project instance
2015-02-11 04:50:02 +00:00
:param manager: Parent VM Manager
2015-01-14 17:52:02 +00:00
:param console: TCP console port
2015-01-21 15:43:34 +00:00
:param startup_script: Content of vpcs startup script file
2015-01-14 17:52:02 +00:00
"""
2015-01-20 12:24:00 +00:00
def __init__(self, name, vm_id, project, manager, console=None, startup_script=None):
2015-02-19 10:33:25 +00:00
super().__init__(name, vm_id, project, manager, console=console)
2015-01-19 10:22:24 +00:00
2015-01-14 17:52:02 +00:00
self._command = []
self._process = None
self._vpcs_stdout_file = ""
self._started = False
# VPCS settings
2015-01-21 15:43:34 +00:00
if startup_script is not None:
self.startup_script = startup_script
2015-01-14 17:52:02 +00:00
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
2015-02-16 16:40:13 +00:00
@asyncio.coroutine
def close(self):
log.debug("VPCS {name} [{id}] is closing".format(name=self._name, id=self._id))
2015-01-22 10:34:10 +00:00
if self._console:
self._manager.port_manager.release_tcp_port(self._console, self._project)
2015-01-22 10:34:10 +00:00
self._console = None
2015-01-20 15:24:46 +00:00
nio = self._ethernet_adapter.get_nio(0)
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
self._terminate_process()
2015-01-20 22:27:28 +00:00
@asyncio.coroutine
def _check_requirements(self):
"""
Check if VPCS is available with the correct version
"""
2015-02-05 11:00:34 +00:00
path = self.vpcs_path
if not path:
2015-01-14 17:52:02 +00:00
raise VPCSError("No path to a VPCS executable has been set")
2015-02-05 11:00:34 +00:00
if not os.path.isfile(path):
raise VPCSError("VPCS program '{}' is not accessible".format(path))
2015-01-14 17:52:02 +00:00
2015-02-05 11:00:34 +00:00
if not os.access(path, os.X_OK):
raise VPCSError("VPCS program '{}' is not executable".format(path))
2015-01-14 17:52:02 +00:00
2015-01-20 22:27:28 +00:00
yield from self._check_vpcs_version()
2015-01-14 17:52:02 +00:00
2015-01-20 18:56:18 +00:00
def __json__(self):
2015-01-21 22:21:15 +00:00
return {"name": self.name,
"vm_id": self.id,
2015-01-20 19:54:46 +00:00
"console": self._console,
"project_id": self.project.id,
2015-02-27 12:36:11 +00:00
"startup_script": self.startup_script,
"startup_script_path": self.relative_startup_script}
@property
def relative_startup_script(self):
"""
Returns the startup config file relative to the project directory.
:returns: path to config file. None if the file doesn't exist
"""
path = os.path.join(self.working_dir, 'startup.vpc')
if os.path.exists(path):
return 'startup.vpc'
else:
return None
2015-01-20 18:56:18 +00:00
@property
def vpcs_path(self):
"""
Returns the VPCS executable path.
:returns: path to VPCS
"""
2015-02-05 11:00:34 +00:00
path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs")
if path == "vpcs":
path = shutil.which("vpcs")
return path
@BaseVM.name.setter
2015-01-14 17:52:02 +00:00
def name(self, new_name):
"""
2015-01-20 12:12:26 +00:00
Sets the name of this VPCS vm.
2015-01-14 17:52:02 +00:00
:param new_name: name
"""
if self.script_file:
2015-01-21 20:46:16 +00:00
content = self.startup_script
content = content.replace(self._name, new_name)
self.startup_script = content
super(VPCSVM, VPCSVM).name.__set__(self, new_name)
2015-01-14 17:52:02 +00:00
2015-01-21 15:43:34 +00:00
@property
def startup_script(self):
"""Return the content of the current startup script"""
script_file = self.script_file
if script_file is None:
return None
2015-01-21 15:43:34 +00:00
try:
with open(script_file) as f:
2015-01-21 15:43:34 +00:00
return f.read()
except OSError as e:
raise VPCSError("Can't read VPCS startup file '{}'".format(script_file))
2015-01-21 15:43:34 +00:00
@startup_script.setter
def startup_script(self, startup_script):
"""
Update the startup script
:param startup_script The content of the vpcs startup script
"""
try:
script_file = os.path.join(self.working_dir, 'startup.vpc')
with open(script_file, 'w+') as f:
2015-01-21 20:46:16 +00:00
if startup_script is None:
f.write('')
else:
2015-02-03 20:48:20 +00:00
startup_script = startup_script.replace("%h", self._name)
2015-01-21 20:46:16 +00:00
f.write(startup_script)
2015-01-21 15:43:34 +00:00
except OSError as e:
raise VPCSError("Can't write VPCS startup file '{}'".format(self.script_file))
2015-01-21 15:43:34 +00:00
2015-01-20 22:27:28 +00:00
@asyncio.coroutine
2015-01-14 17:52:02 +00:00
def _check_vpcs_version(self):
"""
Checks if the VPCS executable version is >= 0.5b1.
"""
try:
2015-02-16 16:20:07 +00:00
output = yield from subprocess_check_output(self.vpcs_path, "-v", cwd=self.working_dir)
2015-01-20 22:27:28 +00:00
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output)
2015-01-14 17:52:02 +00:00
if match:
version = match.group(1)
if parse_version(version) < parse_version("0.5b1"):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else:
2015-02-05 11:00:34 +00:00
raise VPCSError("Could not determine the VPCS version for {}".format(self.vpcs_path))
2015-01-14 17:52:02 +00:00
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.
"""
2015-01-20 22:27:28 +00:00
yield from self._check_requirements()
2015-01-20 21:50:26 +00:00
2015-01-14 17:52:02 +00:00
if not self.is_running():
2015-01-16 15:20:10 +00:00
if not self._ethernet_adapter.get_nio(0):
raise VPCSError("This VPCS instance must be connected in order to start")
2015-01-14 17:52:02 +00:00
self._command = self._build_command()
try:
2015-01-20 15:24:46 +00:00
log.info("Starting VPCS: {}".format(self._command))
self._vpcs_stdout_file = os.path.join(self.working_dir, "vpcs.log")
2015-01-20 15:24:46 +00:00
log.info("Logging to {}".format(self._vpcs_stdout_file))
2015-01-14 17:52:02 +00:00
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,
2015-01-19 21:43:35 +00:00
stdout=fd,
stderr=subprocess.STDOUT,
cwd=self.working_dir,
2015-01-19 21:43:35 +00:00
creationflags=flags)
log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid))
2015-01-14 17:52:02 +00:00
self._started = True
except (OSError, subprocess.SubprocessError) as e:
vpcs_stdout = self.read_vpcs_stdout()
2015-02-05 11:00:34 +00:00
log.error("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout))
raise VPCSError("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout))
2015-01-20 15:24:46 +00:00
2015-01-14 17:52:02 +00:00
@asyncio.coroutine
def stop(self):
"""
Stops the VPCS process.
"""
if self.is_running():
self._terminate_process()
try:
2015-02-10 01:24:13 +00:00
yield from asyncio.wait_for(self._process.wait(), timeout=3)
except asyncio.TimeoutError:
2015-02-10 01:24:13 +00:00
if self._process.returncode is None:
log.warn("VPCS process {} is still running... killing it".format(self._process.pid))
self._process.kill()
2015-01-14 17:52:02 +00:00
self._process = None
self._started = False
2015-01-22 09:55:11 +00:00
@asyncio.coroutine
def reload(self):
"""
Reload the VPCS process. (Stop / Start)
"""
yield from self.stop()
yield from self.start()
def _terminate_process(self):
"""Terminate the process if running"""
2015-01-20 15:24:46 +00:00
if self._process:
log.info("Stopping VPCS instance {} PID={}".format(self.name, self._process.pid))
if sys.platform.startswith("win32"):
self._process.send_signal(signal.CTRL_BREAK_EVENT)
else:
try:
self._process.terminate()
# Sometime the process can already be dead when we garbage collect
except ProcessLookupError:
pass
2015-01-14 17:52:02 +00:00
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_number, nio):
2015-01-14 17:52:02 +00:00
"""
Adds a port NIO binding.
:param port_number: port number
2015-01-14 17:52:02 +00:00
:param nio: NIO instance to add to the slot/port
"""
if not self._ethernet_adapter.port_exists(port_number):
raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
2015-01-14 17:52:02 +00:00
self._ethernet_adapter.add_nio(port_number, nio)
log.info("VPCS {name} [{id}]: {nio} added to port {port_number}".format(name=self._name,
id=self.id,
nio=nio,
port_number=port_number))
2015-01-16 15:20:10 +00:00
return nio
2015-01-14 17:52:02 +00:00
def port_remove_nio_binding(self, port_number):
2015-01-14 17:52:02 +00:00
"""
Removes a port NIO binding.
:param port_number: port number
2015-01-14 17:52:02 +00:00
:returns: NIO instance
"""
if not self._ethernet_adapter.port_exists(port_number):
raise VPCSError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
2015-01-14 17:52:02 +00:00
nio = self._ethernet_adapter.get_nio(port_number)
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
self._ethernet_adapter.remove_nio(port_number)
log.info("VPCS {name} [{id}]: {nio} removed from port {port_number}".format(name=self._name,
id=self.id,
nio=nio,
port_number=port_number))
2015-01-14 17:52:02 +00:00
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:
2015-01-20 12:12:26 +00:00
-d vm device name, works only when -i is set to 1
2015-01-14 17:52:02 +00:00
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.
"""
2015-02-05 11:00:34 +00:00
command = [self.vpcs_path]
2015-01-14 17:52:02 +00:00
command.extend(["-p", str(self._console)]) # listen to console port
nio = self._ethernet_adapter.get_nio(0)
if nio:
if isinstance(nio, NIOUDP):
2015-01-14 17:52:02 +00:00
# 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, NIOTAP):
2015-01-14 17:52:02 +00:00
# TAP interface
command.extend(["-e"])
2015-01-20 12:12:26 +00:00
command.extend(["-d", nio.tap_vm])
2015-01-14 17:52:02 +00:00
command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset
2015-01-14 17:52:02 +00:00
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])
2015-01-14 17:52:02 +00:00
return command
@property
def script_file(self):
"""
Returns the script-file for this VPCS instance.
:returns: path to script-file
"""
# If the default VPCS file exist we use it
path = os.path.join(self.working_dir, 'startup.vpc')
if os.path.exists(path):
return path
else:
return None