# -*- 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 . """ Represents a Dynamips hypervisor and starts/stops the associated Dynamips process. """ import os import subprocess import tempfile import asyncio from .dynamips_hypervisor import DynamipsHypervisor from .dynamips_error import DynamipsError import logging log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): """ Hypervisor. :param path: path to Dynamips executable :param working_dir: working directory :param host: host/address for this hypervisor :param port: port for this hypervisor :param console_host: host/address for console connections """ _instance_count = 1 def __init__(self, path, working_dir, host, port, console_host): super().__init__(working_dir, host, port) # create an unique ID self._id = Hypervisor._instance_count Hypervisor._instance_count += 1 self._console_host = console_host self._path = path self._command = [] self._process = None self._stdout_file = "" self._started = False @property def id(self): """ Returns the unique ID for this hypervisor. :returns: id (integer) """ return self._id @property def started(self): """ Returns either this hypervisor has been started or not. :returns: boolean """ return self._started @property def path(self): """ Returns the path to the Dynamips executable. :returns: path to Dynamips """ return self._path @path.setter def path(self, path): """ Sets the path to the Dynamips executable. :param path: path to Dynamips """ self._path = path @asyncio.coroutine def start(self): """ Starts the Dynamips hypervisor process. """ self._command = self._build_command() try: log.info("Starting Dynamips: {}".format(self._command)) with tempfile.NamedTemporaryFile(delete=False) as fd: self._stdout_file = fd.name log.info("Dynamips process logging to {}".format(fd.name)) self._process = yield from asyncio.create_subprocess_exec(*self._command, stdout=fd, stderr=subprocess.STDOUT, cwd=self._working_dir) log.info("Dynamips process started PID={}".format(self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: log.error("Could not start Dynamips: {}".format(e)) raise DynamipsError("Could not start Dynamips: {}".format(e)) @asyncio.coroutine def stop(self): """ Stops the Dynamips hypervisor process. """ if self.is_running(): log.info("Stopping Dynamips process PID={}".format(self._process.pid)) yield from DynamipsHypervisor.stop(self) # give some time for the hypervisor to properly stop. # time to delete UNIX NIOs for instance. yield from asyncio.sleep(0.01) try: yield from asyncio.wait_for(self._process.wait(), timeout=3) except asyncio.TimeoutError: if self._process.returncode is None: log.warn("Dynamips process {} is still running... killing it".format(self._process.pid)) self._process.kill() if self._stdout_file and os.access(self._stdout_file, os.W_OK): try: os.remove(self._stdout_file) except OSError as e: log.warning("could not delete temporary Dynamips log file: {}".format(e)) self._started = False def read_stdout(self): """ Reads the standard output of the Dynamips process. Only use when the process has been stopped or has crashed. """ output = "" if self._stdout_file and os.access(self._stdout_file, os.R_OK): try: with open(self._stdout_file, errors="replace") as file: output = file.read() except OSError as e: log.warn("could not read {}: {}".format(self._stdout_file, e)) return output def is_running(self): """ Checks if the process is running :returns: True or False """ if self._process and self._process.returncode is None: return True return False def _build_command(self): """ Command to start the Dynamips hypervisor process. (to be passed to subprocess.Popen()) """ command = [self._path] command.extend(["-N1"]) # use instance IDs for filenames command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file # Dynamips cannot listen for hypervisor commands and for console connections on # 2 different IP addresses. # See https://github.com/GNS3/dynamips/issues/62 if self._console_host != "0.0.0.0" and self._console_host != "::": command.extend(["-H", "{}:{}".format(self._host, self._port)]) else: command.extend(["-H", str(self._port)]) return command