Compare commits

..

154 Commits

Author SHA1 Message Date
ba357b0541 Bump version to 1.2.1 2014-12-04 12:49:40 -07:00
f58c7960c9 Use bundled Qemu on Windows and OSX by default and checks if remote server are registered. 2014-12-04 12:25:49 -07:00
5a468888c8 Bump version to 1.2.1.dev2 2014-12-02 18:52:28 -07:00
8f53d51c05 Support for CPU throttling and process priority for Qemu. 2014-12-02 18:12:37 -07:00
1e01c85be9 Change search paths for Qemu on Windows. 2014-12-02 14:49:39 -07:00
fed02ee167 Adds default path for VBoxManage on Mac OS X. 2014-11-29 16:42:57 -07:00
632134a02a Support for older Qemu versions like the 0.11.0 on Windows. 2014-11-29 14:11:51 -07:00
183a6aed44 Do not use universal_newlines in subprocess. 2014-11-26 15:07:15 -07:00
d97ba11728 Fixes C7200 IO cards insert/remove issues and makes C7200-IO-FE the default. 2014-11-24 17:02:00 -07:00
4918675cd5 Fixes Qemu version detection. 2014-11-24 11:44:27 -07:00
6ef614103e Ignore inaccessible VirtualBox VMs. 2014-11-24 11:15:30 -07:00
09948a366f Use SubprocessError to catch Subprocess exceptions. 2014-11-22 17:45:04 -07:00
3bd88178a0 Bump to version 1.2.1.dev1 and fixes vboxmanage lookup on Windows. 2014-11-20 19:01:00 -07:00
95f5c73e33 Bump to version 1.2 2014-11-19 19:28:21 -07:00
fd92189d51 Restore dock widgets. 2014-11-19 10:22:09 -07:00
cb913416ef Bump to version 1.2.dev3 2014-11-15 16:47:30 -07:00
5a7e482dac Linked clone support for VirtualBox (still problems with temporary projects). 2014-11-15 16:05:55 -07:00
2509ee70e8 Merge pull request #51 from DimArmen/patch-1
Update setup.py
2014-11-15 10:21:49 -07:00
a765bed3da Update setup.py
missing comma causes installation to fail...
2014-11-15 18:37:44 +02:00
e2e4f4f38b Fixes remote server issue when creating a new project while already in a project. 2014-11-14 19:59:06 -07:00
e75dde3ebf Merge pull request #48 from planctechnologies/pr3
Add support for Qemu devices on cloud instances (server)
2014-11-12 21:19:15 -07:00
bba2c2b0d3 Merge pull request #47 from planctechnologies/pr2
Support IOU devices on cloud instances
2014-11-12 21:17:57 -07:00
a9e924934a Fixes important issue when searching for a free port. 2014-11-12 19:49:02 -07:00
387896fa69 Merge pull request #10 from planctechnologies/gns-129
Move image path manipulation to server side
2014-11-12 14:22:10 -07:00
4d9d6ae5dd Merge pull request #9 from planctechnologies/gns-125
Add support for Qemu devices on cloud instances
2014-11-12 14:19:58 -07:00
f6561bf684 Automatically extract IOS configs when a project is closed. 2014-11-10 13:50:17 -07:00
5b73786653 Move image path manipulation to server side 2014-11-10 11:28:19 -07:00
f44fbd1f16 Option to allow console connections to any local IP address when using the local server. 2014-11-09 23:01:13 -07:00
1982ff8100 Allows Qemu VM to have 0 interface. 2014-11-09 18:27:40 -07:00
7a6f27fed9 New VirtualBox guest property: ProjectDirInGNS3. 2014-11-09 16:10:30 -07:00
747ca7bb90 Base for VirtualBox linked clones (not completed yet). 2014-11-09 11:50:47 -07:00
faa3ef8cb4 Add support for Qemu devices on cloud instances 2014-11-07 20:42:08 -07:00
f94a900b95 Merge pull request #8 from planctechnologies/gns-123
Support IOU devices on cloud instances
2014-11-06 16:18:27 -07:00
0b0830976f Support IOU devices on cloud instances 2014-11-06 15:50:46 -07:00
31db1a4e84 Merge remote-tracking branch 'origin/master'
Conflicts:
	gns3server/modules/virtualbox/virtualbox_vm.py
2014-11-06 13:59:05 -07:00
e07347a961 Rename "enable console" to "remote console". 2014-11-06 13:56:19 -07:00
a4e20cd6f6 Add VirtualBox guest property "NameInGNS3". 2014-11-06 10:11:39 -07:00
a98a8b1acc Change default VirtualBox adapter type. 2014-11-04 19:00:01 -07:00
7809160ea1 Add detection of qemu and qemu.exe binaries. 2014-11-03 17:36:14 -07:00
410729c998 Check for duplicate node names in Preferences. 2014-11-03 15:06:07 -07:00
3a85e2dba7 Fixes missing cloud settings on Windows. 2014-11-02 18:09:35 -07:00
087f0e82de Fixes issues with VirtualBox Telnet server on Windows. 2014-11-02 18:06:15 -07:00
393a312e7e New Telnet server for VirtualBox. 2014-11-02 15:47:44 -07:00
4d23c5917c Add REUSE flag to socket when scanning for unused ports. 2014-11-01 15:44:18 -06:00
89e80fd74b Merge pull request #43 from planctechnologies/dev
Download IOS images from Cloud Files to a cloud instance
2014-11-01 11:19:12 -06:00
a48aff6ce5 Fixes some issues with VirtualBox support. 2014-10-31 17:41:12 -06:00
e5fa52fcb5 Adding back a line that was mistakenly removed. 2014-10-31 10:26:53 -06:00
ff02bb977a Merge branch 'master' into dev 2014-10-31 10:02:58 -06:00
7b531cf094 Fixes issue when getting the VirtualBox VM list. 2014-10-30 21:10:14 -06:00
dab72cf036 New VirtualBox support (under testing). 2014-10-30 18:53:17 -06:00
bf0b6ee534 Merge pull request #7 from planctechnologies/gns-110
Support launching devices from cloud file images
2014-10-30 14:23:43 -06:00
95a89ac91b Change find an unused port. 2014-10-29 10:15:22 -06:00
f5540ee147 Change find an unused port. 2014-10-28 21:03:51 -06:00
8c47522a18 Merge pull request #39 from planctechnologies/dev
Improve logging, PEP8 cleanup
2014-10-28 16:39:58 -06:00
d2798a969e Cleanup 2014-10-28 11:27:41 -06:00
148b99c553 Cleanup 2014-10-28 11:09:43 -06:00
5f9554b86c Cleanup 2014-10-28 11:07:44 -06:00
3a157b5e6d Handle a missing cloud server section in the config file 2014-10-28 11:01:17 -06:00
7830bf8b1a Merge branch 'dev' into gns-110 2014-10-28 10:39:03 -06:00
ee1dbd6cd3 Merge branch 'master' into dev 2014-10-28 09:38:37 -06:00
c4afc33ea8 IOS devices can be deployed on cloud instances. 2014-10-27 18:12:56 -06:00
f1f44078ba Update README. 2014-10-27 15:58:13 -06:00
88b9d946da Fixes SecureCRT issue when disconnecting from an IOU device on Windows. 2014-10-25 18:03:24 -06:00
20acca64b5 Bump version to 1.2.dev1 2014-10-25 18:01:14 -06:00
440148aa0b Bump version to 1.1 2014-10-22 22:43:48 -06:00
f48c9117b0 Serial console for VirtualBox. 2014-10-22 21:59:11 -06:00
666c8ea922 Pedantic: make sure Idle-PC is spelled that way. 2014-10-22 20:47:59 -06:00
91894935bf Merge branch 'dev' into gns-110 2014-10-21 15:39:14 -06:00
3b3c47c858 Bump version to 1.1.dev1. 2014-10-21 10:02:07 -06:00
f0c344939b Polish and bump up to version 1.0! 2014-10-20 18:40:05 -06:00
e261263aab Add path lookup directory for Qemu on OSX. 2014-10-20 11:56:30 -06:00
6d80d3e70d Merge branch 'master' into dev 2014-10-20 11:22:04 -06:00
b88abb7c91 Remote servers and load-balancing (still things to improve). 2014-10-19 17:29:04 -06:00
c08e1011ed Make the server download images from cloud files 2014-10-15 15:51:00 -06:00
a833925497 Copied fresh cloud files from gns gui repo 2014-10-15 15:50:24 -06:00
5f4b3c547b Bump to version 1.0.dev1. 2014-10-14 17:47:52 -06:00
f854752c84 Bump to version 1.0-beta4. 2014-10-14 17:20:28 -06:00
f287f5141a Update .gitignore file 2014-10-14 13:55:17 -06:00
4195bdc7dd Auto idle-pc feature and improvements/bug fixes for GNS3 preferences. 2014-10-13 19:53:17 -06:00
c0fc093ab7 Merge branch 'master' into dev 2014-10-10 15:24:08 -06:00
b68c11e33e Bump version to 1.0-beta4.dev2 2014-10-09 21:26:07 -06:00
b3e86be182 Merge pull request #37 from planctechnologies/server_security2
Add secure communication between gui and server 2/2
2014-09-30 11:24:28 -06:00
5802c2b9f5 Merge pull request #36 from planctechnologies/server_security
Add secure communication between gui and server 1/2
2014-09-30 11:23:04 -06:00
83cef60c0f Merge pull request #35 from planctechnologies/deadman2
Deadman switch support 2/2
2014-09-30 11:20:39 -06:00
e39c93c91a Merge pull request #34 from planctechnologies/deadman
Deadman switch support 1/2
2014-09-30 11:20:01 -06:00
1a96a150bc Fix shemas for QEMU. 2014-09-30 11:15:15 -06:00
65fdafda40 Merge pull request #6 from planctechnologies/gns-108
Add a --quiet mode to gns3server
2014-09-29 19:55:36 -06:00
c66fbbdb36 Merge improved ssl and auth support 2014-09-29 18:24:28 -06:00
03fb75437b Add cert and auth support to gns3server. 2014-09-29 18:19:35 -06:00
3833803244 Copy deadman switch code from old repository into gns3server. 2014-09-29 18:14:07 -06:00
7c446796fe gns3server now controls the deadman switch. 2014-09-29 18:09:16 -06:00
ee88d6f808 Merge branch 'master' into dev 2014-09-29 17:24:52 -06:00
d4d774055a Remove unused parameter 2014-09-29 16:01:39 -06:00
efc80ff17a Revert version number change 2014-09-29 15:59:49 -06:00
91fba4aca4 Use logging config to set destination of copyright info 2014-09-29 15:56:01 -06:00
46495b9265 Merge remote-tracking branch 'origin/master' 2014-09-28 18:23:51 -06:00
a8193fa063 Split the PATH environment variable using os.pathsep 2014-09-28 18:23:27 -06:00
e3eecb6584 Merge pull request #33 from dlintott/fix_broken_test
Fix test for dynamips c7200 NPE (Default is now NPE-400)
2014-09-27 15:07:20 -06:00
35f3434b2f Merge pull request #32 from noplay/osx_installation
Instruction for development on MacOS X
2014-09-27 13:56:28 -06:00
20dc779fd8 Fix test for dynamips c7200 NPE (Default is now NPE-400) 2014-09-27 19:27:26 +01:00
04f670cb50 Instruction for development on MacOS X 2014-09-27 19:59:58 +02:00
23686215fe Add a --quiet mode to gns3server 2014-09-25 14:42:37 -06:00
6dce005594 Bump to version 1.0-beta1.dev1. 2014-09-24 11:14:28 -06:00
a49f107af2 Bump to version 1.0-beta3. 2014-09-24 11:01:33 -06:00
e7141685cc Tweaks to support Qemu on Windows. 2014-09-23 21:38:51 -06:00
aca9e0de56 Qemu integration stage 2, support for ASA and IDS. 2014-09-22 21:24:55 -06:00
3b465890b6 Increase sleep to work around Rackspace slowness 2014-09-22 09:10:30 -06:00
cf59240bef Bugfixes with cloud server communication 2014-09-21 21:41:51 -06:00
d1715baae1 Base QEMU support. 2014-09-18 15:47:43 -06:00
b132c901c9 Disabling auth from version string 2014-09-18 20:39:12 +00:00
a0e2fe551a Added web user and password to start_server output 2014-09-15 21:25:09 -06:00
800d4d91f9 Merge pull request #5 from planctechnologies/gns3-87-bugfixes
Gns-87 bugfixes
2014-09-11 14:48:26 -06:00
6c6c9200e4 Add CN support to cert as command line arg 2014-09-08 22:07:33 -06:00
4fa87005bc Enabled HTTP Auth, SSL and DMS disabling based on cloud.conf availability 2014-09-08 21:51:56 -06:00
17e4b51d18 Testing out dummy config 2014-09-08 20:45:36 -06:00
6421367259 Importing changeset from gns3dms repo 2014-09-08 15:35:22 +00:00
6ff2c654d9 Merge pull request #4 from planctechnologies/gns3-87
Gns3 87 - add server security and startup scripts
2014-09-08 09:25:28 -06:00
f876a862c4 GNS3 server will now create the heardbeat file durining initialization 2014-09-06 21:13:09 -06:00
ef492d4690 Update gns3dms to support cloud.conf 2014-09-06 20:46:06 -06:00
36e539382c Added support for cloud.conf file and startup script 2014-09-06 00:51:43 -06:00
6f9e0f6d2e Moved certs to .config 2014-09-03 22:19:59 -06:00
b84dda3c8e HTTP auth added to file_upload and jsonrpc 2014-09-03 22:12:34 -06:00
e2f3d2aca8 Pull from dev 2014-09-03 20:39:02 +00:00
382e693fc8 Added authentication handler for basic auth check 2014-09-03 00:05:06 -06:00
a95cc678e9 Added server.py ssl mode dependant on cert existence 2014-09-02 22:33:45 -06:00
bcf0aae531 Added HOME support and cert dir to create_cert script 2014-09-02 22:17:06 -06:00
b483f87c2f Bump version to 1.0-beta3.dev1. 2014-09-02 15:49:39 -06:00
9d2e18328b Bump version to 1.0-beta2. 2014-09-02 13:06:26 -06:00
174013da80 Merge pull request #2 from planctechnologies/deadman
GNS-42 - Move deadman switch into gns3server codebase
2014-08-31 23:50:41 -06:00
99a8f5f21a Added create_cert.sh and ssl_options to enable SSL 2014-08-30 01:32:48 -06:00
5e72fcbe14 GNS-42 - Move deadman switch into gns3server codebase 2014-08-29 18:05:56 +00:00
e688d96c36 Add start, stop, restart and heartbeat handling to DeadMan module 2014-08-28 23:06:28 -06:00
3845cab84b Adding initial module 2014-08-28 22:09:38 -06:00
98e3a2e088 Updated requirements.txt 2014-08-28 22:01:23 -06:00
76b357c1ce Do not activate sparse memory by default for c1700 and c2600 platforms.
https://github.com/GNS3/dynamips/issues/54
2014-08-26 17:07:48 -06:00
80ab81190c Add "enable console" option to VirtualBox VMs (True by default).
Add "start at" option to VirtualBox VMs (adapter start index, 0 by default).
2014-08-26 15:27:43 -06:00
934404cc90 Change default port ranges. 2014-08-25 15:40:04 -06:00
6e39630b9b Required VirtualBox wrapper is >= 9.1 2014-08-22 17:39:57 -06:00
569a68a486 VirtualBox support refactoring. 2014-08-22 17:36:12 -06:00
77c583ca39 Check if the VirtualBox COM service is installed on Windows. 2014-08-21 18:13:41 -06:00
ea05744e1c Force to rebuild the COM cache on Windows (for VirtualBox support). 2014-08-17 15:15:07 -06:00
e0f0c98ffd Do not look for vboxwrapper on non Windows platforms. 2014-08-13 12:11:41 -06:00
a8d740ef21 Fix version from 1.0beta2-dev1 to 1.0beta2.dev1 2014-08-11 22:13:21 -06:00
90c8c4312c Merge pull request #27 from dlintott/master
Override check_origin and fix test suite
2014-08-10 15:27:49 -06:00
e5642546f1 Remove commented line, not needed anymore 2014-08-09 12:26:24 +01:00
4a33b2021c Further optimise the Travis testing and improve running tests for a user
+ Convert setup.py test to run py.test instead of tox
  + Tox should now run setup.py test
  + TravisCI will create a job for each TOX_ENV and then execute
    tox to run the tests for that TOX_ENV
2014-08-09 12:05:31 +01:00
a4bc96af28 revert not installing requirements outside of tox 2014-08-08 19:20:20 +01:00
d8f622d438 Streamline TravisCI build
+ As we use Tox there's no need to run seperate builds for
   python3.3 and python3.4
 + There's no need to install the requirements in main
   environment as all dependencies are handled in the Tox
   virtualenv's
2014-08-08 19:15:26 +01:00
ad287d3434 Remove un-needed imports 2014-08-08 19:14:36 +01:00
4a4a57e1a3 Further test fixes
+ tests/dynamips/test_hypervisor.py: Increase sleep time to prevent
   random test failures
 + tests/iou/test_iou_device.py: Rework test skipping based on presence
   of IOU image rather than environment variable
2014-08-08 17:54:30 +01:00
9b010d6388 Update test_hypervisor.py
+ test_stdout: use system dynamips
 + test_stdout: correct host address to start dynamips on
2014-08-08 15:00:44 +01:00
8fc4667d2c Modify the TRAVIS environment check 2014-08-08 14:49:10 +01:00
7cbce0f81b Fix test suite
+ Install VPCS and dynamips from GNS3 PPA
 + Drop netifaces-py3 from requirements.txt
 + Fix/update tests to use installed VPCS and dynamips
2014-08-08 14:32:32 +01:00
578bb5741d Override check_origin from tornado.websocket 2014-08-06 22:43:37 +01:00
81 changed files with 5972 additions and 3542 deletions

3
.gitignore vendored
View File

@ -35,5 +35,8 @@ nosetests.xml
.pydevproject
.settings
# Pycharm
.idea
# Gedit Backup Files
*~

View File

@ -1,14 +1,19 @@
language: python
python:
- "3.3"
- "3.4"
env:
- TOX_ENV=py33
- TOX_ENV=py34
before_install:
- sudo add-apt-repository ppa:gns3/ppa -y
- sudo apt-get update -q
install:
- "pip install -r requirements.txt --use-mirrors"
- "pip install tox"
- pip install tox
- sudo apt-get install vpcs dynamips
script: "python setup.py test"
script:
- tox -e $TOX_ENV
branches:
only:

View File

@ -1,24 +1,37 @@
GNS3-server
===========
New GNS3 server repository (alpha stage).
This is the GNS3 server repository.
The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM.
Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets.
You will need the new GNS3 GUI (gns3-gui repository) to control the server.
You will need the GNS3 GUI (gns3-gui repository) to control the server.
Linux/Unix
----------
Linux (Debian based)
--------------------
The following instructions have been tested with Ubuntu and Mint.
You must be connected to the Internet in order to install the dependencies.
Dependencies:
- Python version 3.3 or above
- pip & setuptools must be installed, please see http://pip.readthedocs.org/en/latest/installing.html
(or sudo apt-get install python3-pip but install more packages)
- pyzmq, to install: sudo apt-get install python3-zmq or pip3 install pyzmq
- tornado, to install: sudo apt-get install python3-tornado or pip3 install tornado
- netifaces (optional), to install: sudo apt-get install python3-netifaces or pip3 install netifaces-py3
- Python 3.3 or above
- Setuptools
- PyZMQ library
- Netifaces library
- Tornado
- Jsonschema
The following commands will install some of these dependencies:
.. code:: bash
sudo apt-get install python3-setuptools
sudo apt-get install python3-zmq
sudo apt-get install python3-netifaces
Finally these commands will install the server as well as the rest of the dependencies:
.. code:: bash
@ -34,4 +47,18 @@ Please use our all-in-one installer.
Mac OS X
--------
Please use our DMG package.
Please use our DMG package for a simple installation.
If you want to test the current git version or contribute to the project.
You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/
and homebrew: http://brew.sh/.
.. code:: bash
brew install python3
mkvirtualenv gns3-server --python=/usr/local/bin/python3.4
python3 setup.py install
gns3server

View File

@ -101,8 +101,9 @@ def main():
startup_script)
passwd = uuid.uuid4().hex
instance.change_password(passwd)
# wait for the password change to be processed
sleep(POLL_SEC)
# wait for the password change to be processed. Continuing while
# a password change is processing will cause image creation to fail.
sleep(POLL_SEC*6)
env.host_string = str(instance.accessIPv4)
env.user = "root"

View File

@ -11,9 +11,7 @@ mkdir -p /opt/gns3
pushd /opt/gns3
git clone --branch ${git_branch} ${git_url}
cd gns3-server
pip3 install tornado
pip3 install pyzmq
pip3 install jsonschema
pip3 install -r dev-requirements.txt
python3 ./setup.py install
${rc_local}

26
gns3dms/__init__.py Normal file
View File

@ -0,0 +1,26 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
from .version import __version__

View File

View File

@ -0,0 +1,289 @@
# -*- 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/>.
"""
Base cloud controller class.
Base class for interacting with Cloud APIs to create and manage cloud
instances.
"""
from collections import namedtuple
import hashlib
import os
import logging
from io import StringIO, BytesIO
from libcloud.compute.base import NodeAuthSSHKey
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
from .exceptions import Unauthorized, ApiError
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
log = logging.getLogger(__name__)
def parse_exception(exception):
"""
Parse the exception to separate the HTTP status code from the text.
Libcloud raises many exceptions of the form:
Exception("<http status code> <http error> <reponse body>")
in lieu of raising specific incident-based exceptions.
"""
e_str = str(exception)
try:
status = int(e_str[0:3])
error_text = e_str[3:]
except ValueError:
status = None
error_text = e_str
return status, error_text
class BaseCloudCtrl(object):
""" Base class for interacting with a cloud provider API. """
http_status_to_exception = {
400: BadRequest,
401: Unauthorized,
404: ItemNotFound,
405: MethodNotAllowed,
413: OverLimit,
500: ApiError,
503: ServiceUnavailable
}
GNS3_CONTAINER_NAME = 'GNS3'
def __init__(self, username, api_key):
self.username = username
self.api_key = api_key
def _handle_exception(self, status, error_text, response_overrides=None):
""" Raise an exception based on the HTTP status. """
if response_overrides:
if status in response_overrides:
raise response_overrides[status](error_text)
raise self.http_status_to_exception[status](error_text)
def authenticate(self):
""" Validate cloud account credentials. Return boolean. """
raise NotImplementedError
def list_sizes(self):
""" Return a list of NodeSize objects. """
return self.driver.list_sizes()
def list_flavors(self):
""" Return an iterable of flavors """
raise NotImplementedError
def create_instance(self, name, size_id, image_id, keypair):
"""
Create a new instance with the supplied attributes.
Return a Node object.
"""
try:
image = self.get_image(image_id)
if image is None:
raise ItemNotFound("Image not found")
size = self.driver.ex_get_size(size_id)
args = {
"name": name,
"size": size,
"image": image,
}
if keypair is not None:
auth_key = NodeAuthSSHKey(keypair.public_key)
args["auth"] = auth_key
args["ex_keyname"] = name
return self.driver.create_node(**args)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
log.error("create_instance method raised an exception: {}".format(e))
log.error('image id {}'.format(image))
def delete_instance(self, instance):
""" Delete the specified instance. Returns True or False. """
try:
return self.driver.destroy_node(instance)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def get_instance(self, instance):
""" Return a Node object representing the requested instance. """
for i in self.driver.list_nodes():
if i.id == instance.id:
return i
raise ItemNotFound("Instance not found")
def list_instances(self):
""" Return a list of instances in the current region. """
try:
return self.driver.list_nodes()
except Exception as e:
log.error("list_instances returned an error: {}".format(e))
def create_key_pair(self, name):
""" Create and return a new Key Pair. """
response_overrides = {
409: KeyPairExists
}
try:
return self.driver.create_key_pair(name)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text, response_overrides)
else:
raise e
def delete_key_pair(self, keypair):
""" Delete the keypair. Returns True or False. """
try:
return self.driver.delete_key_pair(keypair)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def delete_key_pair_by_name(self, keypair_name):
""" Utility method to incapsulate boilerplate code """
kp = KeyPair(name=keypair_name)
return self.delete_key_pair(kp)
def list_key_pairs(self):
""" Return a list of Key Pairs. """
return self.driver.list_key_pairs()
def upload_file(self, file_path, folder):
"""
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
:param file_path: path to file to upload
:param folder: folder in cloud storage to save file in
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
"""
try:
gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME)
except ContainerAlreadyExistsError:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
with open(file_path, 'rb') as file:
local_file_hash = hashlib.md5(file.read()).hexdigest()
cloud_object_name = folder + '/' + os.path.basename(file_path)
cloud_hash_name = cloud_object_name + '.md5'
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
# if the file and its hash are in object storage, and the local and storage file hashes match
# do not upload the file, otherwise upload it
if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects:
hash_object = gns3_container.get_object(cloud_hash_name)
cloud_object_hash = ''
for chunk in hash_object.as_stream():
cloud_object_hash += chunk.decode('utf8')
if cloud_object_hash == local_file_hash:
return False
file.seek(0)
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
return True
def list_projects(self):
"""
Lists projects in cloud storage
:return: List of (project name, object name in storage)
"""
try:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
projects = [
(obj.name.replace('projects/', '').replace('.zip', ''), obj.name)
for obj in gns3_container.list_objects()
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
]
return projects
except ContainerDoesNotExistError:
return []
def download_file(self, file_name, destination=None):
"""
Downloads file from cloud storage
:param file_name: name of file in cloud storage to download
:param destination: local path to save file to (if None, returns file contents as a file-like object)
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
"""
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
storage_object = gns3_container.get_object(file_name)
if destination is not None:
storage_object.download(destination)
else:
contents = b''
for chunk in storage_object.as_stream():
contents += chunk
return BytesIO(contents)

View File

@ -0,0 +1,45 @@
""" Exception classes for CloudCtrl classes. """
class ApiError(Exception):
""" Raised when the server returns 500 Compute Error. """
pass
class BadRequest(Exception):
""" Raised when the server returns 400 Bad Request. """
pass
class ComputeFault(Exception):
""" Raised when the server returns 400|500 Compute Fault. """
pass
class Forbidden(Exception):
""" Raised when the server returns 403 Forbidden. """
pass
class ItemNotFound(Exception):
""" Raised when the server returns 404 Not Found. """
pass
class KeyPairExists(Exception):
""" Raised when the server returns 409 Conflict Key pair exists. """
pass
class MethodNotAllowed(Exception):
""" Raised when the server returns 405 Method Not Allowed. """
pass
class OverLimit(Exception):
""" Raised when the server returns 413 Over Limit. """
pass
class ServerCapacityUnavailable(Exception):
""" Raised when the server returns 503 Server Capacity Uavailable. """
pass
class ServiceUnavailable(Exception):
""" Raised when the server returns 503 Service Unavailable. """
pass
class Unauthorized(Exception):
""" Raised when the server returns 401 Unauthorized. """
pass

View File

@ -0,0 +1,311 @@
# -*- 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/>.
""" Interacts with Rackspace API to create and manage cloud instances. """
from .base_cloud_ctrl import BaseCloudCtrl
import json
import requests
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
from libcloud.compute.providers import get_driver
from libcloud.compute.types import Provider
from libcloud.storage.providers import get_driver as get_storage_driver
from libcloud.storage.types import Provider as StorageProvider
from .exceptions import ItemNotFound, ApiError
from ..version import __version__
from collections import OrderedDict
import logging
log = logging.getLogger(__name__)
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
ENDPOINT_ARGS_MAP]
class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key, gns3_ias_url):
super(RackspaceCtrl, self).__init__(username, api_key)
self.gns3_ias_url = gns3_ias_url
# set this up so it can be swapped out with a mock for testing
self.post_fn = requests.post
self.driver_cls = get_driver(Provider.RACKSPACE)
self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
self.driver = None
self.storage_driver = None
self.region = None
self.instances = {}
self.authenticated = False
self.identity_ep = \
"https://identity.api.rackspacecloud.com/v2.0/tokens"
self.regions = []
self.token = None
self.tenant_id = None
self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors"
self._flavors = OrderedDict([
('2', '512MB, 1 VCPU'),
('3', '1GB, 1 VCPU'),
('4', '2GB, 2 VCPUs'),
('5', '4GB, 2 VCPUs'),
('6', '8GB, 4 VCPUs'),
('7', '15GB, 6 VCPUs'),
('8', '30GB, 8 VCPUs'),
('performance1-1', '1GB Performance, 1 VCPU'),
('performance1-2', '2GB Performance, 2 VCPUs'),
('performance1-4', '4GB Performance, 4 VCPUs'),
('performance1-8', '8GB Performance, 8 VCPUs'),
('performance2-15', '15GB Performance, 4 VCPUs'),
('performance2-30', '30GB Performance, 8 VCPUs'),
('performance2-60', '60GB Performance, 16 VCPUs'),
('performance2-90', '90GB Performance, 24 VCPUs'),
('performance2-120', '120GB Performance, 32 VCPUs',)
])
def authenticate(self):
"""
Submit username and api key to API service.
If authentication is successful, set self.regions and self.token.
Return boolean.
"""
self.authenticated = False
if len(self.username) < 1:
return False
if len(self.api_key) < 1:
return False
data = json.dumps({
"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.username,
"apiKey": self.api_key
}
}
})
headers = {
'Content-type': 'application/json',
'Accept': 'application/json'
}
response = self.post_fn(self.identity_ep, data=data, headers=headers)
if response.status_code == 200:
api_data = response.json()
self.token = self._parse_token(api_data)
if self.token:
self.authenticated = True
user_regions = self._parse_endpoints(api_data)
self.regions = self._make_region_list(user_regions)
self.tenant_id = self._parse_tenant_id(api_data)
else:
self.regions = []
self.token = None
response.connection.close()
return self.authenticated
def list_regions(self):
""" Return a list the regions available to the user. """
return self.regions
def list_flavors(self):
""" Return the dictionary containing flavors id and names """
return self._flavors
def _parse_endpoints(self, api_data):
"""
Parse the JSON-encoded data returned by the Identity Service API.
Return a list of regions available for Compute v2.
"""
region_codes = []
for ep_type in api_data['access']['serviceCatalog']:
if ep_type['name'] == "cloudServersOpenStack" \
and ep_type['type'] == "compute":
for ep in ep_type['endpoints']:
if ep['versionId'] == "2":
region_codes.append(ep['region'])
return region_codes
def _parse_token(self, api_data):
""" Parse the token from the JSON-encoded data returned by the API. """
try:
token = api_data['access']['token']['id']
except KeyError:
return None
return token
def _parse_tenant_id(self, api_data):
""" """
try:
roles = api_data['access']['user']['roles']
for role in roles:
if 'tenantId' in role and role['name'] == 'compute:default':
return role['tenantId']
return None
except KeyError:
return None
def _make_region_list(self, region_codes):
"""
Make a list of regions for use in the GUI.
Returns a list of key-value pairs in the form:
<API's Region Name>: <libcloud's Region Name>
eg,
[
{'DFW': 'dfw'}
{'ORD': 'ord'},
...
]
"""
region_list = []
for ep in ENDPOINT_ARGS_MAP:
if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes:
region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep})
return region_list
def set_region(self, region):
""" Set self.region and self.driver. Returns True or False. """
try:
self.driver = self.driver_cls(self.username, self.api_key,
region=region)
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
region=region)
except ValueError:
return False
self.region = region
return True
def _get_shared_images(self, username, region, gns3_version):
"""
Given a GNS3 version, ask gns3-ias to share compatible images
Response:
[{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},]
or, if access was already asked
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
"""
endpoint = self.gns3_ias_url+"/images/grant_access"
params = {
"user_id": username,
"user_region": region.upper(),
"gns3_version": gns3_version,
}
try:
response = requests.get(endpoint, params=params)
except requests.ConnectionError:
raise ApiError("Unable to connect to IAS")
status = response.status_code
if status == 200:
return response.json()
elif status == 404:
raise ItemNotFound()
else:
raise ApiError("IAS status code: %d" % status)
def list_images(self):
"""
Return a dictionary containing RackSpace server images
retrieved from gns3-ias server
"""
if not (self.tenant_id and self.region):
return {}
try:
shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
images = {}
for i in shared_images:
images[i['image_id']] = i['image_name']
return images
except ItemNotFound:
return {}
except ApiError as e:
log.error('Error while retrieving image list: %s' % e)
return {}
def get_image(self, image_id):
return self.driver.get_image(image_id)
def get_provider(cloud_settings):
"""
Utility function to retrieve a cloud provider instance already authenticated and with the
region set
:param cloud_settings: cloud settings dictionary
:return: a provider instance or None on errors
"""
try:
username = cloud_settings['cloud_user_name']
apikey = cloud_settings['cloud_api_key']
region = cloud_settings['cloud_region']
ias_url = cloud_settings.get('gns3_ias_url', '')
except KeyError as e:
log.error("Unable to create cloud provider: {}".format(e))
return
provider = RackspaceCtrl(username, apikey, ias_url)
if not provider.authenticate():
log.error("Authentication failed for cloud provider")
return
if not region:
region = provider.list_regions().values()[0]
if not provider.set_region(region):
log.error("Unable to set cloud provider region")
return
return provider

401
gns3dms/main.py Normal file
View File

@ -0,0 +1,401 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Monitors communication with the GNS3 client via tmp file. Will terminate the instance if
communication is lost.
"""
import os
import sys
import time
import getopt
import datetime
import logging
import signal
import configparser
from logging.handlers import *
from os.path import expanduser
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
EXTRA_LIB = "%s/modules" % (SCRIPT_PATH)
sys.path.append(EXTRA_LIB)
from . import cloud
from rackspace_cloud import Rackspace
LOG_NAME = "gns3dms"
log = None
sys.path.append(EXTRA_LIB)
import daemon
my_daemon = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--cloud_api_key <api_key> Rackspace API key
--cloud_user_name
--instance_id ID of the Rackspace instance to terminate
--cloud_region Region of instance
--deadtime How long in seconds can the communication lose exist before we
shutdown this instance.
Default:
Example --deadtime=3600 (60 minutes)
--check-interval Defaults to --deadtime, used for debugging
--init-wait Inital wait time, how long before we start pulling the file.
Default: 300 (5 min)
Example --init-wait=300
--file The file we monitor for updates
-k Kill previous instance running in background
--background Run in background
""" % (SCRIPT_NAME)
# Parse cmd line options
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Pass in cmd line arguments
"""
short_args = "dvhk"
long_args = ("debug",
"verbose",
"help",
"cloud_user_name=",
"cloud_api_key=",
"instance_id=",
"region=",
"deadtime=",
"init-wait=",
"check-interval=",
"file=",
"background",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {}
cmd_line_option_list["debug"] = False
cmd_line_option_list["verbose"] = True
cmd_line_option_list["cloud_user_name"] = None
cmd_line_option_list["cloud_api_key"] = None
cmd_line_option_list["instance_id"] = None
cmd_line_option_list["region"] = None
cmd_line_option_list["deadtime"] = 60 * 60 #minutes
cmd_line_option_list["check-interval"] = None
cmd_line_option_list["init-wait"] = 5 * 60
cmd_line_option_list["file"] = None
cmd_line_option_list["shutdown"] = False
cmd_line_option_list["daemon"] = False
cmd_line_option_list['starttime'] = datetime.datetime.now()
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
get_gns3secrets(cmd_line_option_list)
for opt, val in opts:
if (opt in ("-h", "--help")):
print(usage)
sys.exit(0)
elif (opt in ("-d", "--debug")):
cmd_line_option_list["debug"] = True
elif (opt in ("-v", "--verbose")):
cmd_line_option_list["verbose"] = True
elif (opt in ("--cloud_user_name")):
cmd_line_option_list["cloud_user_name"] = val
elif (opt in ("--cloud_api_key")):
cmd_line_option_list["cloud_api_key"] = val
elif (opt in ("--instance_id")):
cmd_line_option_list["instance_id"] = val
elif (opt in ("--region")):
cmd_line_option_list["region"] = val
elif (opt in ("--deadtime")):
cmd_line_option_list["deadtime"] = int(val)
elif (opt in ("--check-interval")):
cmd_line_option_list["check-interval"] = int(val)
elif (opt in ("--init-wait")):
cmd_line_option_list["init-wait"] = int(val)
elif (opt in ("--file")):
cmd_line_option_list["file"] = val
elif (opt in ("-k")):
cmd_line_option_list["shutdown"] = True
elif (opt in ("--background")):
cmd_line_option_list["daemon"] = True
if cmd_line_option_list["shutdown"] == False:
if cmd_line_option_list["check-interval"] is None:
cmd_line_option_list["check-interval"] = cmd_line_option_list["deadtime"] + 120
if cmd_line_option_list["cloud_user_name"] is None:
print("You need to specify a username!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["cloud_api_key"] is None:
print("You need to specify an apikey!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["file"] is None:
print("You need to specify a file to watch!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["instance_id"] is None:
print("You need to specify an instance_id")
print(usage)
sys.exit(2)
if cmd_line_option_list["cloud_region"] is None:
print("You need to specify a cloud_region")
print(usage)
sys.exit(2)
return cmd_line_option_list
def get_gns3secrets(cmd_line_option_list):
"""
Load cloud credentials from .gns3secrets
"""
gns3secret_paths = [
os.path.join(os.path.expanduser("~"), '.config', 'GNS3'),
SCRIPT_PATH,
]
config = configparser.ConfigParser()
for gns3secret_path in gns3secret_paths:
gns3secret_file = "%s/cloud.conf" % (gns3secret_path)
if os.path.isfile(gns3secret_file):
config.read(gns3secret_file)
try:
for key, value in config.items("CLOUD_SERVER"):
cmd_line_option_list[key] = value.strip()
except configparser.NoSectionError:
pass
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] == True:
log_level_console = logging.INFO
if cmd_options['debug'] == True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_hndlr = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
return log
def send_shutdown(pid_file):
"""
Sends the daemon process a kill signal
"""
try:
with open(pid_file, 'r') as pidf:
pid = int(pidf.readline().strip())
pidf.close()
os.kill(pid, 15)
except:
log.info("No running instance found!!!")
log.info("Missing PID file: %s" % (pid_file))
def _get_file_age(filename):
return datetime.datetime.fromtimestamp(
os.path.getmtime(filename)
)
def monitor_loop(options):
"""
Checks the options["file"] modification time against an interval. If the
modification time is too old we terminate the instance.
"""
log.debug("Waiting for init-wait to pass: %s" % (options["init-wait"]))
time.sleep(options["init-wait"])
log.info("Starting monitor_loop")
terminate_attempts = 0
while options['shutdown'] == False:
log.debug("In monitor_loop for : %s" % (
datetime.datetime.now() - options['starttime'])
)
file_last_modified = _get_file_age(options["file"])
now = datetime.datetime.now()
delta = now - file_last_modified
log.debug("File last updated: %s seconds ago" % (delta.seconds))
if delta.seconds > options["deadtime"]:
log.warning("Deadtime exceeded, terminating instance ...")
#Terminate involes many layers of HTTP / API calls, lots of
#different errors types could occur here.
try:
rksp = Rackspace(options)
rksp.terminate()
except Exception as e:
log.critical("Exception during terminate: %s" % (e))
terminate_attempts+=1
log.warning("Termination sent, attempt: %s" % (terminate_attempts))
time.sleep(600)
else:
time.sleep(options["check-interval"])
log.info("Leaving monitor_loop")
log.info("Shutting down")
def main():
global log
global my_daemon
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
options["shutdown"] = True
pid_file = "%s/.gns3dms.pid" % (expanduser("~"))
if options["shutdown"]:
send_shutdown(pid_file)
sys.exit(0)
if options["daemon"]:
my_daemon = MyDaemon(pid_file, options)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
log.info("Starting ...")
log.debug("Using settings:")
for key, value in iter(sorted(options.items())):
log.debug("%s : %s" % (key, value))
log.debug("Checking file ....")
if os.path.isfile(options["file"]) == False:
log.critical("File does not exist!!!")
sys.exit(1)
test_acess = _get_file_age(options["file"])
if type(test_acess) is not datetime.datetime:
log.critical("Can't get file modification time!!!")
sys.exit(1)
if my_daemon:
my_daemon.start()
else:
monitor_loop(options)
class MyDaemon(daemon.daemon):
def run(self):
monitor_loop(self.options)
if __name__ == "__main__":
result = main()
sys.exit(result)

View File

@ -0,0 +1,24 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)

138
gns3dms/modules/daemon.py Normal file
View File

@ -0,0 +1,138 @@
"""Generic linux daemon base class for python 3.x."""
import sys, os, time, atexit, signal
class daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile, options):
self.pidfile = pidfile
self.options = options
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def delpid(self):
os.remove(self.pidfile)
def check_pid(self, pid):
""" Check For the existence of a unix pid. """
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
pid_exist = self.check_pid(pid)
if pid_exist:
message = "Already running: %s\n" % (pid)
sys.stderr.write(message)
sys.exit(1)
else:
message = "pidfile {0} already exist. " + \
"but process is dead\n"
sys.stderr.write(message.format(self.pidfile))
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""

View File

@ -0,0 +1,70 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
import os, sys
import json
import logging
import socket
from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl
LOG_NAME = "gns3dms.rksp"
log = logging.getLogger("%s" % (LOG_NAME))
class Rackspace(object):
def __init__(self, options):
self.username = options["cloud_user_name"]
self.apikey = options["cloud_api_key"]
self.authenticated = False
self.hostname = socket.gethostname()
self.instance_id = options["instance_id"]
self.region = options["region"]
log.debug("Authenticating with Rackspace")
log.debug("My hostname: %s" % (self.hostname))
self.rksp = RackspaceCtrl(self.username, self.apikey)
self.authenticated = self.rksp.authenticate()
def _find_my_instance(self):
if self.authenticated == False:
log.critical("Not authenticated against rackspace!!!!")
for region in self.rksp.list_regions():
log.debug("Rackspace regions: %s" % (region))
log.debug("Checking region: %s" % (self.region))
self.rksp.set_region(self.region)
for server in self.rksp.list_instances():
log.debug("Checking server: %s" % (server.name))
if server.name.lower() == self.hostname.lower() and server.id == self.instance_id:
log.info("Found matching instance: %s" % (server.id))
log.info("Startup id: %s" % (self.instance_id))
return server
def terminate(self):
server = self._find_my_instance()
log.warning("Sending termination")
self.rksp.delete_instance(server)

27
gns3dms/version.py Normal file
View File

@ -0,0 +1,27 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "0.1"
__version_info__ = (0, 0, 1, -99)

View File

@ -0,0 +1,99 @@
#!/bin/bash
# -*- 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/>.
# Bash shell script for generating self-signed certs. Run this in a folder, as it
# generates a few files. Large portions of this script were taken from the
# following artcile:
#
# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html
#
# Additional alterations by: Brad Landers
# Date: 2012-01-27
# https://gist.github.com/bradland/1690807
# Script accepts a single argument, the fqdn for the cert
DST_DIR="$HOME/.config/GNS3Certs/"
OLD_DIR=`pwd`
#GNS3 Server expects to find certs with the default FQDN below. If you create
#different certs you will need to update server.py
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
DOMAIN="gns3server.localdomain.com"
fi
fail_if_error() {
[ $1 != 0 ] && {
unset PASSPHRASE
cd $OLD_DIR
exit 10
}
}
mkdir -p $DST_DIR
fail_if_error $?
cd $DST_DIR
# Generate a passphrase
export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo)
# Certificate details; replace items in angle brackets with your own info
subj="
C=CA
ST=Alberta
O=GNS3
localityName=Calgary
commonName=$DOMAIN
organizationalUnitName=GNS3Server
emailAddress=gns3cert@gns3.com
"
# Generate the server private key
openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048
fail_if_error $?
#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE
# Generate the CSR
openssl req \
-new \
-batch \
-subj "$(echo -n "$subj" | tr "\n" "/")" \
-key $DOMAIN.key \
-out $DOMAIN.csr \
-passin env:PASSPHRASE
fail_if_error $?
cp $DOMAIN.key $DOMAIN.key.org
fail_if_error $?
# Strip the password so we don't have to type it every time we restart Apache
openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE
fail_if_error $?
# Generate the cert (good for 10 years)
openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt
fail_if_error $?
echo "${DST_DIR}${DOMAIN}.key"
echo "${DST_DIR}${DOMAIN}.crt"
cd $OLD_DIR

View File

@ -26,6 +26,8 @@ import configparser
import logging
log = logging.getLogger(__name__)
CLOUD_SERVER = 'CLOUD_SERVER'
class Config(object):
"""
@ -46,12 +48,14 @@ class Config(object):
appdata = os.path.expandvars("%APPDATA%")
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
self._cloud_file = os.path.join(appdata, appname, "cloud.ini")
filename = "server.ini"
self._files = [os.path.join(appdata, appname, filename),
os.path.join(appdata, appname + ".ini"),
os.path.join(common_appdata, appname, filename),
os.path.join(common_appdata, appname + ".ini"),
filename]
filename,
self._cloud_file]
else:
# On UNIX-like platforms, the configuration file location can be one of the following:
@ -62,15 +66,30 @@ class Config(object):
# 5: server.conf in the current working directory
home = os.path.expanduser("~")
self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf")
filename = "server.conf"
self._files = [os.path.join(home, ".config", appname, filename),
os.path.join(home, ".config", appname + ".conf"),
os.path.join("/etc/xdg", appname, filename),
os.path.join("/etc/xdg", appname + ".conf"),
filename]
filename,
self._cloud_file]
self._config = configparser.ConfigParser()
self.read_config()
self._cloud_config = configparser.ConfigParser()
self.read_cloud_config()
def list_cloud_config_file(self):
return self._cloud_file
def read_cloud_config(self):
parsed_file = self._cloud_config.read(self._cloud_file)
if not self._cloud_config.has_section(CLOUD_SERVER):
self._cloud_config.add_section(CLOUD_SERVER)
def cloud_settings(self):
return self._cloud_config[CLOUD_SERVER]
def read_config(self):
"""

View File

@ -0,0 +1,92 @@
# -*- 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/>.
"""
Simple file upload & listing handler.
"""
import os
import tornado.web
import tornado.websocket
import logging
log = logging.getLogger(__name__)
class GNS3BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.write('<html><body><form action="/login" method="post">'
'Name: <input type="text" name="name">'
'Password: <input type="text" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
try:
redirect_to = self.get_argument("next")
self.set_secure_cookie("login_success_redirect_to", redirect_to)
except tornado.web.MissingArgumentError:
pass
def post(self):
user = self.get_argument("name")
password = self.get_argument("password")
if self.settings['required_user'] == user and self.settings['required_pass'] == password:
self.set_secure_cookie("user", user)
auth_status = "successful"
else:
self.set_secure_cookie("user", "None")
auth_status = "failure"
log.info("Authentication attempt {}: {}, {}".format(auth_status, user, password))
try:
redirect_to = self.get_secure_cookie("login_success_redirect_to")
except tornado.web.MissingArgumentError:
redirect_to = "/"
if redirect_to is None:
self.write({'result': auth_status})
else:
log.info('Redirecting to {}'.format(redirect_to))
self.redirect(redirect_to)

View File

@ -23,6 +23,7 @@ Simple file upload & listing handler.
import os
import stat
import tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
from ..config import Config
@ -30,7 +31,7 @@ import logging
log = logging.getLogger(__name__)
class FileUploadHandler(tornado.web.RequestHandler):
class FileUploadHandler(GNS3BaseHandler):
"""
File upload handler.
@ -54,6 +55,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
except OSError as e:
log.error("could not create the upload directory {}: {}".format(self._upload_dir, e))
@tornado.web.authenticated
def get(self):
"""
Invoked on GET request.
@ -70,6 +72,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
path=path,
items=items)
@tornado.web.authenticated
def post(self):
"""
Invoked on POST request.

View File

@ -22,6 +22,7 @@ JSON-RPC protocol over Websockets.
import zmq
import uuid
import tornado.websocket
from .auth_handler import GNS3WebSocketBaseHandler
from tornado.escape import json_decode
from ..jsonrpc import JSONRPCParseError
from ..jsonrpc import JSONRPCInvalidRequest
@ -33,7 +34,7 @@ import logging
log = logging.getLogger(__name__)
class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
class JSONRPCWebSocket(GNS3WebSocketBaseHandler):
"""
STOMP protocol over Tornado Websockets with message
routing to ZeroMQ dealer clients.
@ -52,6 +53,9 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
self._session_id = str(uuid.uuid4())
self.zmq_router = zmq_router
def check_origin(self, origin):
return True
@property
def session_id(self):
"""
@ -116,7 +120,15 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
"""
log.info("Websocket client {} connected".format(self.session_id))
self.clients.add(self)
authenticated_user = self.get_current_user()
if authenticated_user:
self.clients.add(self)
log.info("Websocket authenticated user: %s" % (authenticated_user))
else:
self.close()
log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user))
def on_message(self, message):
"""

View File

@ -15,11 +15,11 @@
# 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 tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
class VersionHandler(tornado.web.RequestHandler):
class VersionHandler(GNS3BaseHandler):
def get(self):
response = {'version': __version__}

View File

@ -34,6 +34,8 @@ define("host", default="0.0.0.0", help="run on the given host/IP address", type=
define("port", default=8000, help="run on the given port", type=int)
define("ipc", default=False, help="use IPC for module communication", type=bool)
define("version", default=False, help="show the version", type=bool)
define("quiet", default=False, help="do not show output on stdout", type=bool)
define("console_bind_to_any", default=True, help="bind console ports to any local IP address", type=bool)
def locale_check():
@ -96,17 +98,25 @@ def main():
raise SystemExit
current_year = datetime.date.today().year
print("GNS3 server version {}".format(__version__))
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
user_log = logging.getLogger('user_facing')
if not options.quiet:
# Send user facing messages to stdout.
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.addFilter(logging.Filter(name='user_facing'))
user_log.addHandler(stream_handler)
user_log.propagate = False
user_log.info("GNS3 server version {}".format(__version__))
user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
# we only support Python 3 version >= 3.3
if sys.version_info < (3, 3):
raise RuntimeError("Python 3.3 or higher is required")
print("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0],
minor=sys.version_info[1],
micro=sys.version_info[2],
pid=os.getpid()))
user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(
major=sys.version_info[0], minor=sys.version_info[1],
micro=sys.version_info[2], pid=os.getpid()))
# check for the correct locale
# (UNIX/Linux only)
@ -118,9 +128,7 @@ def main():
log.critical("the current working directory doesn't exist")
return
server = Server(options.host,
options.port,
ipc=options.ipc)
server = Server(options.host, options.port, options.ipc, options.console_bind_to_any)
server.load_modules()
server.run()

View File

@ -17,11 +17,13 @@
import sys
from .base import IModule
from .deadman import DeadMan
from .dynamips import Dynamips
from .qemu import Qemu
from .vpcs import VPCS
from .virtualbox import VirtualBox
MODULES = [Dynamips, VPCS, VirtualBox]
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux

View File

@ -50,6 +50,7 @@ def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP",
else:
socket_type = socket.SOCK_STREAM
last_exception = None
for port in range(start_port, end_port + 1):
if port in ignore_ports:
continue
@ -57,21 +58,21 @@ def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP",
if ":" in host:
# IPv6 address support
with socket.socket(socket.AF_INET6, socket_type) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port)) # the port is available if bind is a success
else:
with socket.socket(socket.AF_INET, socket_type) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port)) # the port is available if bind is a success
return port
except OSError as e:
if e.errno == errno.EADDRINUSE: # socket already in use
if port + 1 == end_port:
break
else:
continue
last_exception = e
if port + 1 == end_port:
break
else:
raise Exception("Could not find an unused port: {}".format(e))
continue
raise Exception("Could not find a free port between {0} and {1}".format(start_port, end_port))
raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, end_port, host, last_exception))
def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):

View File

@ -61,6 +61,7 @@ class IModule(multiprocessing.Process):
self._current_destination = None
self._current_call_id = None
self._stopping = False
self._cloud_settings = config.cloud_settings()
def _setup(self):
"""

View File

@ -0,0 +1,164 @@
# -*- 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/>.
"""
DeadMan server module.
"""
import os
import time
import subprocess
from gns3server.modules import IModule
from gns3server.config import Config
import logging
log = logging.getLogger(__name__)
class DeadMan(IModule):
"""
DeadMan module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
config = Config.instance()
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._host = kwargs["host"]
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % (
self._tempdir)
if 'heartbeat_file' in kwargs:
self._heartbeat_file = kwargs['heartbeat_file']
self._is_enabled = False
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
instance_id = cloud_config["instance_id"]
cloud_user_name = cloud_config["cloud_user_name"]
cloud_api_key = cloud_config["cloud_api_key"]
self._is_enabled = True
except KeyError:
log.critical("Missing cloud.conf - disabling Deadman Switch")
self._deadman_process = None
self.heartbeat()
self.start()
def _start_deadman_process(self):
"""
Start a subprocess and return the object
"""
#gnsserver gets configuration options from cloud.conf. This is where
#the client adds specific cloud information.
#gns3dms also reads in cloud.conf. That is why we don't need to specific
#all the command line arguments here.
cmd = []
cmd.append("gns3dms")
cmd.append("--file")
cmd.append("%s" % (self._heartbeat_file))
cmd.append("--background")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False)
return process
def _stop_deadman_process(self):
"""
Start a subprocess and return the object
"""
cmd = []
cmd.append("gns3dms")
cmd.append("-k")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False)
return process
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
if self._deadman_process == None:
log.info("Deadman: Can't stop, is not currently running")
log.debug("Deadman: Stopping process")
self._deadman_process = self._stop_deadman_process()
self._deadman_process = None
#Jerry or Jeremy why do we do this? Won't this stop the I/O loop for
#for everyone?
IModule.stop(self, signum) # this will stop the I/O loop
def start(self, request=None):
"""
Start the deadman process on the server
"""
if self._is_enabled:
self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting")
@IModule.route("deadman.reset")
def reset(self, request=None):
"""
Resets the module (JSON-RPC notification).
:param request: JSON request (not used)
"""
self.stop()
self.start()
log.info("Deadman: Module has been reset")
@IModule.route("deadman.heartbeat")
def heartbeat(self, request=None):
"""
Update a file on the server that the deadman switch will monitor
"""
now = time.time()
with open(self._heartbeat_file, 'w') as heartbeat_file:
heartbeat_file.write(str(now))
heartbeat_file.close()
log.debug("Deadman: heartbeat_file updated: %s %s" % (
self._heartbeat_file,
now,
))
self.start()

View File

@ -33,7 +33,6 @@ from gns3server.builtins.interfaces import get_windows_interfaces
from .hypervisor import Hypervisor
from .hypervisor_manager import HypervisorManager
from .dynamips_error import DynamipsError
from ..attic import has_privileged_access
# Nodes
from .nodes.router import Router
@ -111,7 +110,7 @@ class Dynamips(IModule):
dynamips_config = config.get_section_config(name.upper())
self._dynamips = dynamips_config.get("dynamips_path")
if not self._dynamips or not os.path.isfile(self._dynamips):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Dynamips in the current working directory and $PATH
for path in paths:
try:
@ -137,7 +136,8 @@ class Dynamips(IModule):
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._host = kwargs["host"]
self._host = dynamips_config.get("host", kwargs["host"])
self._console_host = dynamips_config.get("console_host", kwargs["console_host"])
if not sys.platform.startswith("win32"):
#FIXME: pickle issues Windows
@ -154,12 +154,19 @@ class Dynamips(IModule):
if not sys.platform.startswith("win32"):
self._callback.stop()
# automatically save configs for all router instances
for router_id in self._routers:
router = self._routers[router_id]
try:
router.save_configs()
except DynamipsError:
continue
# stop all Dynamips hypervisors
if self._hypervisor_manager:
self._hypervisor_manager.stop_all_hypervisors()
self.delete_dynamips_files()
IModule.stop(self, signum) # this will stop the I/O loop
def _check_hypervisors(self):
@ -225,6 +232,14 @@ class Dynamips(IModule):
:param request: JSON request (not used)
"""
# automatically save configs for all router instances
for router_id in self._routers:
router = self._routers[router_id]
try:
router.save_configs()
except DynamipsError:
continue
# stop all Dynamips hypervisors
if self._hypervisor_manager:
self._hypervisor_manager.stop_all_hypervisors()
@ -254,6 +269,7 @@ class Dynamips(IModule):
self.delete_dynamips_files()
self._hypervisor_manager = None
self._working_dir = self._projects_dir
log.info("dynamips module has been reset")
def start_hypervisor_manager(self):
@ -282,7 +298,7 @@ class Dynamips(IModule):
raise DynamipsError("Cannot write to working directory {}".format(workdir))
log.info("starting the hypervisor manager with Dynamips working directory set to '{}'".format(workdir))
self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host)
self._hypervisor_manager = HypervisorManager(self._dynamips, workdir, self._host, self._console_host)
for name, value in self._hypervisor_manager_settings.items():
if hasattr(self._hypervisor_manager, name) and getattr(self._hypervisor_manager, name) != value:
@ -293,10 +309,8 @@ class Dynamips(IModule):
"""
Set or update settings.
Mandatory request parameters:
- path (path to the Dynamips executable)
Optional request parameters:
- path (path to the Dynamips executable)
- working_dir (path to a working directory)
- project_name
@ -311,7 +325,9 @@ class Dynamips(IModule):
#TODO: JSON schema validation
if not self._hypervisor_manager:
self._dynamips = request.pop("path")
if "path" in request:
self._dynamips = request.pop("path")
if "working_dir" in request:
self._working_dir = request.pop("working_dir")
@ -347,8 +363,13 @@ class Dynamips(IModule):
# for local server
new_working_dir = request.pop("working_dir")
try:
self._hypervisor_manager.working_dir = new_working_dir
except DynamipsError as e:
log.error("could not change working directory: {}".format(e))
return
self._working_dir = new_working_dir
self._hypervisor_manager.working_dir = new_working_dir
# apply settings to the hypervisor manager
for name, value in request.items():

View File

@ -17,7 +17,10 @@
import os
import base64
import ntpath
import time
from gns3server.modules import IModule
from gns3dms.cloud.rackspace_ctrl import get_provider
from ..dynamips_error import DynamipsError
from ..nodes.c1700 import C1700
@ -61,6 +64,7 @@ from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA
from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA
from ..schemas.vm import VM_EXPORT_CONFIG_SCHEMA
from ..schemas.vm import VM_IDLEPCS_SCHEMA
from ..schemas.vm import VM_AUTO_IDLEPC_SCHEMA
from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.vm import VM_ADD_NIO_SCHEMA
from ..schemas.vm import VM_DELETE_NIO_SCHEMA
@ -69,8 +73,8 @@ import logging
log = logging.getLogger(__name__)
ADAPTER_MATRIX = {"C7200_IO_2FE": C7200_IO_2FE,
"C7200_IO_FE": C7200_IO_FE,
ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE,
"C7200-IO-FE": C7200_IO_FE,
"C7200-IO-GE-E": C7200_IO_GE_E,
"NM-16ESW": NM_16ESW,
"NM-1E": NM_1E,
@ -138,12 +142,25 @@ class VM(object):
chassis = request.get("chassis")
router_id = request.get("router_id")
# Locate the image
updated_image_path = os.path.join(self.images_directory, image)
if os.path.isfile(updated_image_path):
image = updated_image_path
else:
if not os.path.exists(self.images_directory):
os.mkdir(self.images_directory)
cloud_path = request.get("cloud_path", None)
if cloud_path is not None:
# Download the image from cloud files
_, filename = ntpath.split(image)
src = '{}/{}'.format(cloud_path, filename)
provider = get_provider(self._cloud_settings)
log.debug("Downloading file from {} to {}...".format(src, updated_image_path))
provider.download_file(src, updated_image_path)
log.debug("Download of {} complete.".format(src))
image = updated_image_path
try:
if platform not in PLATFORMS:
raise DynamipsError("Unknown router platform: {}".format(platform))
@ -158,7 +175,8 @@ class VM(object):
router = PLATFORMS[platform](hypervisor, name, router_id)
router.ram = ram
router.image = image
router.sparsemem = self._hypervisor_manager.sparse_memory_support
if platform not in ("c1700", "c2600"):
router.sparsemem = self._hypervisor_manager.sparse_memory_support
router.mmap = self._hypervisor_manager.mmap_support
if "console" in request:
router.console = request["console"]
@ -450,7 +468,7 @@ class VM(object):
except DynamipsError as e:
self.send_custom_error(str(e))
return
elif name.startswith("slot") and value == None:
elif name.startswith("slot") and value is None:
slot_id = int(name[-1])
if router.slots[slot_id]:
try:
@ -594,31 +612,7 @@ class VM(object):
return
try:
if router.startup_config or router.private_config:
startup_config_base64, private_config_base64 = router.extract_config()
if startup_config_base64:
try:
config = base64.decodestring(startup_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(router.hypervisor.working_dir, router.startup_config)
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(router.startup_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64:
try:
config = base64.decodestring(private_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(router.hypervisor.working_dir, router.private_config)
with open(config_path, "w") as f:
log.info("saving private-config to {}".format(router.private_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e))
router.save_configs()
except DynamipsError as e:
log.warn("could not save config to {}: {}".format(router.startup_config, e))
@ -664,17 +658,17 @@ class VM(object):
@IModule.route("dynamips.vm.idlepcs")
def vm_idlepcs(self, request):
"""
Get idle-pc proposals.
Get Idle-PC proposals.
Mandatory request parameters:
- id (vm identifier)
Optional request parameters:
- compute (returns previously compute idle-pc values if False)
- compute (returns previously compute Idle-PC values if False)
Response parameters:
- id (vm identifier)
- idlepcs (idle-pc values in an array)
- idlepcs (Idle-PC values in an array)
:param request: JSON request
"""
@ -692,7 +686,7 @@ class VM(object):
if "compute" in request and request["compute"] == False:
idlepcs = router.show_idle_pc_prop()
else:
# reset the current idle-pc value before calculating a new one
# reset the current Idle-PC value before calculating a new one
router.idlepc = "0x0"
idlepcs = router.get_idle_pc_prop()
except DynamipsError as e:
@ -703,6 +697,76 @@ class VM(object):
"idlepcs": idlepcs}
self.send_response(response)
@IModule.route("dynamips.vm.auto_idlepc")
def vm_auto_idlepc(self, request):
"""
Auto Idle-PC calculation.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- id (vm identifier)
- logs (logs for the calculation)
- idlepc (Idle-PC value)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_AUTO_IDLEPC_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
try:
router.idlepc = "0x0" # reset the current Idle-PC value before calculating a new one
was_auto_started = False
if router.get_status() != "running":
router.start()
was_auto_started = True
time.sleep(20) # leave time to the router to boot
logs = []
validated_idlepc = "0x0"
idlepcs = router.get_idle_pc_prop()
if not idlepcs:
logs.append("No Idle-PC values found")
for idlepc in idlepcs:
router.idlepc = idlepc.split()[0]
logs.append("Trying Idle-PC value {}".format(router.idlepc))
start_time = time.time()
initial_cpu_usage = router.get_cpu_usage()
logs.append("Initial CPU usage = {}%".format(initial_cpu_usage))
time.sleep(4) # wait 4 seconds to probe the cpu again
elapsed_time = time.time() - start_time
cpu_elapsed_usage = router.get_cpu_usage() - initial_cpu_usage
cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time)
logs.append("CPU usage after {:.2} seconds = {:.2}%".format(elapsed_time, cpu_usage))
if cpu_usage > 100:
cpu_usage = 100
if cpu_usage < 70:
validated_idlepc = router.idlepc
logs.append("Idle-PC value {} has been validated".format(validated_idlepc))
break
except DynamipsError as e:
self.send_custom_error(str(e))
return
finally:
if was_auto_started:
router.stop()
validated_idlepc = "0x0"
response = {"id": router.id,
"logs": logs,
"idlepc": validated_idlepc}
self.send_response(response)
@IModule.route("dynamips.vm.allocate_udp_port")
def vm_allocate_udp_port(self, request):
"""

View File

@ -212,7 +212,7 @@ class Hypervisor(DynamipsHypervisor):
cwd=self._working_dir)
log.info("Dynamips started PID={}".format(self._process.pid))
self._started = True
except OSError as e:
except subprocess.SubprocessError as e:
log.error("could not start Dynamips: {}".format(e))
raise DynamipsError("could not start Dynamips: {}".format(e))

View File

@ -40,14 +40,16 @@ class HypervisorManager(object):
:param path: path to the Dynamips executable
:param working_dir: path to a working directory
:param host: host/address for hypervisors to listen to
:param console_host: IP address to bind for console connections
"""
def __init__(self, path, working_dir, host='127.0.0.1'):
def __init__(self, path, working_dir, host='127.0.0.1', console_host='0.0.0.0'):
self._hypervisors = []
self._path = path
self._working_dir = working_dir
self._host = host
self._console_host = console_host
self._host = console_host # FIXME: Dynamips must be patched to bind on a different address than the one used by the hypervisor.
config = Config.instance()
dynamips_config = config.get_section_config("DYNAMIPS")

View File

@ -51,6 +51,7 @@ class C1700(Router):
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False
if chassis != "1720":
self.chassis = chassis
@ -72,7 +73,8 @@ class C1700(Router):
"disk1": self._disk1,
"chassis": self._chassis,
"iomem": self._iomem,
"clock_divisor": self._clock_divisor}
"clock_divisor": self._clock_divisor,
"sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)

View File

@ -66,6 +66,7 @@ class C2600(Router):
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False
if chassis != "2610":
self.chassis = chassis
@ -87,7 +88,8 @@ class C2600(Router):
"disk1": self._disk1,
"iomem": self._iomem,
"chassis": self._chassis,
"clock_divisor": self._clock_divisor}
"clock_divisor": self._clock_divisor,
"sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)

View File

@ -22,9 +22,8 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294
from ..dynamips_error import DynamipsError
from .router import Router
from ..adapters.c7200_io_2fe import C7200_IO_2FE
from ..adapters.c7200_io_fe import C7200_IO_FE
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
from pkg_resources import parse_version
import logging
log = logging.getLogger(__name__)
@ -55,10 +54,6 @@ class C7200(Router):
if npe != "npe-400":
self.npe = npe
if parse_version(hypervisor.version) <= parse_version('0.2.13'):
# work around a bug when rebooting a router with NPE-400 in Dynamips <= 0.2.13
self.npe = "npe-200"
# 4 sensors with a default temperature of 22C:
# sensor 1 = I/0 controller inlet
# sensor 2 = I/0 controller outlet
@ -75,7 +70,7 @@ class C7200(Router):
if npe == "npe-g2":
self.slot_add_binding(0, C7200_IO_GE_E())
else:
self.slot_add_binding(0, C7200_IO_2FE())
self.slot_add_binding(0, C7200_IO_FE())
def defaults(self):
"""

View File

@ -26,6 +26,7 @@ from ...attic import find_unused_port
import time
import sys
import os
import base64
import logging
log = logging.getLogger(__name__)
@ -598,6 +599,35 @@ class Router(object):
log.info("router {name} [id={id}]: new private-config pushed".format(name=self._name,
id=self._id))
def save_configs(self):
"""
Saves the startup-config and private-config to files.
"""
if self.startup_config or self.private_config:
startup_config_base64, private_config_base64 = self.extract_config()
if startup_config_base64:
try:
config = base64.decodebytes(startup_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(self.hypervisor.working_dir, self.startup_config)
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(self.startup_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64:
try:
config = base64.decodebytes(private_config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config_path = os.path.join(self.hypervisor.working_dir, self.private_config)
with open(config_path, "w") as f:
log.info("saving private-config to {}".format(self.private_config))
f.write(config)
except OSError as e:
raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e))
@property
def ram(self):
"""
@ -804,10 +834,10 @@ class Router(object):
# router is not running
raise DynamipsError("router {name} is not running".format(name=self._name))
log.info("router {name} [id={id}] has started calculating idle-pc values".format(name=self._name, id=self._id))
log.info("router {name} [id={id}] has started calculating Idle-PC values".format(name=self._name, id=self._id))
begin = time.time()
idlepcs = self._hypervisor.send("vm get_idle_pc_prop {} 0".format(self._name))
log.info("router {name} [id={id}] has finished calculating idle-pc values after {time:.4f} seconds".format(name=self._name,
log.info("router {name} [id={id}] has finished calculating Idle-PC values after {time:.4f} seconds".format(name=self._name,
id=self._id,
time=time.time() - begin))
return idlepcs
@ -1255,6 +1285,15 @@ class Router(object):
return (self._hypervisor.send("{platform} show_hardware {name}".format(platform=self._platform,
name=self._name)))
def get_cpu_usage(self):
"""
Returns the CPU usage.
:return: CPU usage in percent
"""
return int(self._hypervisor.send("vm cpu_usage {name} 0".format(name=self._name))[0])
def get_slot_bindings(self):
"""
Returns slot bindings.
@ -1285,7 +1324,7 @@ class Router(object):
adapter=current_adapter))
# Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running
if self.is_running() and not (self._platform == 'c7200'
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter,
@ -1330,7 +1369,7 @@ class Router(object):
slot_id=slot_id))
# Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running
if self.is_running() and not (self._platform == 'c7200'
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter,

View File

@ -67,6 +67,10 @@ VM_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
}
},
"additionalProperties": False,
@ -200,7 +204,7 @@ VM_UPDATE_SCHEMA = {
"type": "integer"
},
"idlepc": {
"description": "idle-pc value",
"description": "Idle-PC value",
"type": "string",
"pattern": "^(0x[0-9a-fA-F]+)?$"
},
@ -471,7 +475,7 @@ VM_EXPORT_CONFIG_SCHEMA = {
VM_IDLEPCS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to calculate or show idle-pcs for VM instance",
"description": "Request validation to calculate or show Idle-PCs for VM instance",
"type": "object",
"properties": {
"id": {
@ -479,7 +483,7 @@ VM_IDLEPCS_SCHEMA = {
"type": "integer"
},
"compute": {
"description": "indicates to compute new idle-pc values",
"description": "indicates to compute new Idle-PC values",
"type": "boolean"
},
},
@ -487,6 +491,20 @@ VM_IDLEPCS_SCHEMA = {
"required": ["id"]
}
VM_AUTO_IDLEPC_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request an auto Idle-PC calculation for this VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a VM instance",

View File

@ -21,12 +21,15 @@ IOU server module.
import os
import base64
import ntpath
import stat
import tempfile
import socket
import shutil
from gns3server.modules import IModule
from gns3server.config import Config
from gns3dms.cloud.rackspace_ctrl import get_provider
from .iou_device import IOUDevice
from .iou_error import IOUError
from .nios.nio_udp import NIO_UDP
@ -68,7 +71,7 @@ class IOU(IModule):
iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap_path")
if not self._iouyap or not os.path.isfile(self._iouyap):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for iouyap in the current working directory and $PATH
for path in paths:
try:
@ -87,11 +90,12 @@ class IOU(IModule):
IModule.__init__(self, name, *args, **kwargs)
self._iou_instances = {}
self._console_start_port_range = iou_config.get("console_start_port_range", 4001)
self._console_end_port_range = iou_config.get("console_end_port_range", 4512)
self._console_end_port_range = iou_config.get("console_end_port_range", 4500)
self._allocated_udp_ports = []
self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001)
self._udp_end_port_range = iou_config.get("udp_end_port_range", 40001)
self._host = kwargs["host"]
self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000)
self._host = iou_config.get("host", kwargs["host"])
self._console_host = iou_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
@ -192,6 +196,7 @@ class IOU(IModule):
self._allocated_udp_ports.clear()
self.delete_iourc_file()
self._working_dir = self._projects_dir
log.info("IOU module has been reset")
@IModule.route("iou.settings")
@ -298,14 +303,30 @@ class IOU(IModule):
updated_iou_path = os.path.join(self.images_directory, iou_path)
if os.path.isfile(updated_iou_path):
iou_path = updated_iou_path
else:
if not os.path.exists(self.images_directory):
os.mkdir(self.images_directory)
cloud_path = request.get("cloud_path", None)
if cloud_path is not None:
# Download the image from cloud files
_, filename = ntpath.split(iou_path)
src = '{}/{}'.format(cloud_path, filename)
provider = get_provider(self._cloud_settings)
log.debug("Downloading file from {} to {}...".format(src, updated_iou_path))
provider.download_file(src, updated_iou_path)
log.debug("Download of {} complete.".format(src))
# Make file executable
st = os.stat(updated_iou_path)
os.chmod(updated_iou_path, st.st_mode | stat.S_IEXEC)
iou_path = updated_iou_path
try:
iou_instance = IOUDevice(name,
iou_path,
self._working_dir,
self._host,
iou_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)

View File

@ -49,9 +49,9 @@ class IOUDevice(object):
:param name: name of this IOU device
:param path: path to IOU executable
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param iou_id: IOU instance ID
: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
"""
@ -63,9 +63,9 @@ class IOUDevice(object):
name,
path,
working_dir,
host="127.0.0.1",
iou_id = None,
iou_id=None,
console=None,
console_host="0.0.0.0",
console_start_port_range=4001,
console_end_port_range=4512):
@ -99,8 +99,8 @@ class IOUDevice(object):
self._iouyap_stdout_file = ""
self._ioucon_thead = None
self._ioucon_thread_stop_event = None
self._host = host
self._started = False
self._console_host = console_host
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
@ -127,7 +127,7 @@ class IOUDevice(object):
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
self._console_host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise IOUError(e)
@ -484,7 +484,7 @@ class IOUDevice(object):
"""
if not self._ioucon_thead:
telnet_server = "{}:{}".format(self._host, self.console)
telnet_server = "{}:{}".format(self._console_host, self.console)
log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
self._ioucon_thread_stop_event = threading.Event()
@ -509,7 +509,7 @@ class IOUDevice(object):
cwd=self._working_dir)
log.info("iouyap started PID={}".format(self._iouyap_process.pid))
except OSError as e:
except subprocess.SubprocessError as e:
iouyap_stdout = self.read_iouyap_stdout()
log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout))
raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout))
@ -521,7 +521,7 @@ class IOUDevice(object):
try:
output = subprocess.check_output(["ldd", self._path])
except (FileNotFoundError, subprocess.CalledProcessError) as e:
except (FileNotFoundError, subprocess.SubprocessError) as e:
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
return
@ -583,7 +583,7 @@ class IOUDevice(object):
self._started = True
except FileNotFoundError as e:
raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e))
except OSError as e:
except subprocess.SubprocessError as e:
iou_stdout = self.read_iou_stdout()
log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
@ -761,7 +761,7 @@ class IOUDevice(object):
command.extend(["-l"])
else:
raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path)))
except (OSError, subprocess.CalledProcessError) as e:
except subprocess.SubprocessError as e:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
def _build_command(self):

View File

@ -224,6 +224,8 @@ class TelnetServer(Console):
buf = self._read_cur(bufsize, socket.MSG_DONTWAIT)
except BlockingIOError:
return None
except ConnectionResetError:
buf = b''
if not buf:
self._disconnect(fileno)

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_GenericEthernet(NIO):
"""
NIO generic Ethernet NIO.
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_TAP(NIO):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -40,6 +40,10 @@ IOU_CREATE_SCHEMA = {
"description": "path to the IOU executable",
"type": "string",
"minLength": 1,
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
}
},
"additionalProperties": False,

View File

@ -0,0 +1,671 @@
# -*- 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/>.
"""
QEMU server module.
"""
import sys
import os
import socket
import shutil
import subprocess
import re
from gns3server.modules import IModule
from gns3server.config import Config
from .qemu_vm import QemuVM
from .qemu_error import QemuError
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
from .schemas import QEMU_CREATE_SCHEMA
from .schemas import QEMU_DELETE_SCHEMA
from .schemas import QEMU_UPDATE_SCHEMA
from .schemas import QEMU_START_SCHEMA
from .schemas import QEMU_STOP_SCHEMA
from .schemas import QEMU_SUSPEND_SCHEMA
from .schemas import QEMU_RELOAD_SCHEMA
from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import QEMU_ADD_NIO_SCHEMA
from .schemas import QEMU_DELETE_NIO_SCHEMA
import logging
log = logging.getLogger(__name__)
class Qemu(IModule):
"""
QEMU module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._qemu_instances = {}
config = Config.instance()
qemu_config = config.get_section_config(name.upper())
self._console_start_port_range = qemu_config.get("console_start_port_range", 5001)
self._console_end_port_range = qemu_config.get("console_end_port_range", 5500)
self._allocated_udp_ports = []
self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001)
self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500)
self._host = qemu_config.get("host", kwargs["host"])
self._console_host = qemu_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
IModule.stop(self, signum) # this will stop the I/O loop
def get_qemu_instance(self, qemu_id):
"""
Returns a QEMU VM instance.
:param qemu_id: QEMU VM identifier
:returns: QemuVM instance
"""
if qemu_id not in self._qemu_instances:
log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1)
self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id))
return None
return self._qemu_instances[qemu_id]
@IModule.route("qemu.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
# resets the instance IDs
QemuVM.reset()
self._qemu_instances.clear()
self._allocated_udp_ports.clear()
self._working_dir = self._projects_dir
log.info("QEMU module has been reset")
@IModule.route("qemu.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- working_dir (path to a working directory)
- project_name
- console_start_port_range
- console_end_port_range
- udp_start_port_range
- udp_end_port_range
:param request: JSON request
"""
if request is None:
self.send_param_error()
return
if "working_dir" in request:
new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir))
else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
try:
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
return
# update the working directory if it has changed
if self._working_dir != new_working_dir:
self._working_dir = new_working_dir
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id))
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
self._console_end_port_range = request["console_end_port_range"]
if "udp_start_port_range" in request and "udp_end_port_range" in request:
self._udp_start_port_range = request["udp_start_port_range"]
self._udp_end_port_range = request["udp_end_port_range"]
log.debug("received request {}".format(request))
@IModule.route("qemu.create")
def qemu_create(self, request):
"""
Creates a new QEMU VM instance.
Mandatory request parameters:
- name (QEMU VM name)
- qemu_path (path to the Qemu binary)
Optional request parameters:
- console (QEMU VM console port)
Response parameters:
- id (QEMU VM instance identifier)
- name (QEMU VM name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_CREATE_SCHEMA):
return
name = request["name"]
qemu_path = request["qemu_path"]
console = request.get("console")
qemu_id = request.get("qemu_id")
try:
qemu_instance = QemuVM(name,
qemu_path,
self._working_dir,
self._host,
qemu_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)
except QemuError as e:
self.send_custom_error(str(e))
return
response = {"name": qemu_instance.name,
"id": qemu_instance.id}
defaults = qemu_instance.defaults()
response.update(defaults)
self._qemu_instances[qemu_instance.id] = qemu_instance
self.send_response(response)
@IModule.route("qemu.delete")
def qemu_delete(self, request):
"""
Deletes a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.clean_delete()
del self._qemu_instances[request["id"]]
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.update")
def qemu_update(self, request):
"""
Updates a QEMU VM instance
Mandatory request parameters:
- id (QEMU VM instance identifier)
Optional request parameters:
- any setting to update
Response parameters:
- updated settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_UPDATE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
# update the QEMU VM settings
response = {}
for name, value in request.items():
if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value:
try:
setattr(qemu_instance, name, value)
response[name] = value
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("qemu.start")
def qemu_start(self, request):
"""
Starts a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_START_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.start()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.reload")
def qemu_reload(self, request):
"""
Reloads a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_RELOAD_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.reload()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.suspend")
def qemu_suspend(self, request):
"""
Suspends a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_SUSPEND_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.suspend()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (QEMU VM identifier)
- port_id (unique port identifier)
Response parameters:
- port_id (unique port identifier)
- lport (allocated local port)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name,
qemu_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("qemu.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
Response parameters:
- port_id (unique port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = None
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
if not nio:
raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except QemuError as e:
self.send_custom_error(str(e))
return
try:
qemu_instance.port_add_nio_binding(port, nio)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("qemu.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = qemu_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
def _get_qemu_version(self, qemu_path):
"""
Gets the Qemu version.
:param qemu_path: path to Qemu
"""
if sys.platform.startswith("win"):
return ""
try:
output = subprocess.check_output([qemu_path, "-version"])
match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
return version
else:
raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
except subprocess.SubprocessError as e:
raise QemuError("Error while looking for the Qemu version: {}".format(e))
@IModule.route("qemu.qemu_list")
def qemu_list(self, request):
"""
Gets QEMU binaries list.
Response parameters:
- List of Qemu binaries
"""
qemus = []
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Qemu binaries in the current working directory and $PATH
if sys.platform.startswith("win"):
# add specific Windows paths
if hasattr(sys, "frozen"):
# add any qemu dir in the same location as gns3server.exe to the list of paths
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
for f in os.listdir(exec_dir):
if f.lower().startswith("qemu"):
paths.append(os.path.join(exec_dir, f))
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
elif sys.platform.startswith("darwin"):
# add specific locations on Mac OS X regardless of what's in $PATH
paths.extend(["/usr/local/bin", "/opt/local/bin"])
if hasattr(sys, "frozen"):
paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
for path in paths:
try:
for f in os.listdir(path):
if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and \
os.access(os.path.join(path, f), os.X_OK) and \
os.path.isfile(os.path.join(path, f)):
qemu_path = os.path.join(path, f)
version = self._get_qemu_version(qemu_path)
qemus.append({"path": qemu_path, "version": version})
except OSError:
continue
response = {"qemus": qemus}
self.send_response(response)
@IModule.route("qemu.echo")
def echo(self, request):
"""
Echo end point for testing purposes.
:param request: JSON request
"""
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
self.send_response(request)

View 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

View 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):
"""
QEMU Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "QEMU Ethernet adapter"

View File

View File

@ -0,0 +1,65 @@
# -*- 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/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
Network Input/Output.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
def startPacketCapture(self, pcap_output_file):
"""
:param pcap_output_file: PCAP destination file for the capture
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
@property
def capturing(self):
"""
Returns either a capture is configured on this NIO.
:returns: boolean
"""
return self._capturing
@property
def pcap_output_file(self):
"""
Returns the path to the PCAP output file.
:returns: path to the PCAP output file
"""
return self._pcap_output_file

View File

@ -0,0 +1,75 @@
# -*- 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.
"""
from .nio import NIO
class NIO_UDP(NIO):
"""
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):
NIO.__init__(self)
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"

View File

@ -0,0 +1,39 @@
# -*- 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/>.
"""
Custom exceptions for QEMU module.
"""
class QemuError(Exception):
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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,411 @@
# -*- 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/>.
QEMU_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new QEMU VM instance",
"type": "object",
"properties": {
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "Path to QEMU",
"type": "string",
"minLength": 1,
},
"qemu_id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
},
"additionalProperties": False,
"required": ["name", "qemu_path"],
}
QEMU_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "path to QEMU",
"type": "string",
"minLength": 1,
},
"hda_disk_image": {
"description": "QEMU hda disk image path",
"type": "string",
},
"hdb_disk_image": {
"description": "QEMU hdb disk image path",
"type": "string",
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"adapters": {
"description": "number of adapters",
"type": "integer",
"minimum": 0,
"maximum": 8,
},
"adapter_type": {
"description": "QEMU adapter type",
"type": "string",
"minLength": 1,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"initrd": {
"description": "QEMU initrd path",
"type": "string",
},
"kernel_image": {
"description": "QEMU kernel image path",
"type": "string",
},
"kernel_command_line": {
"description": "QEMU kernel command line",
"type": "string",
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
},
"legacy_networking": {
"description": "Use QEMU legagy networking commands (-net syntax)",
"type": "boolean",
},
"cpu_throttling": {
"description": "Percentage of CPU allowed for QEMU",
"minimum": 0,
"maximum": 800,
"type": "integer",
},
"process_priority": {
"description": "Process priority for QEMU",
"enum": ["realtime",
"very high",
"high",
"normal",
"low",
"very low"]
},
"options": {
"description": "Additional QEMU options",
"type": "string",
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
QEMU_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a QEMU VM instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 8
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
}
QEMU_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 8
},
},
"additionalProperties": False,
"required": ["id", "port"]
}

View File

@ -23,12 +23,12 @@ import sys
import os
import socket
import shutil
import subprocess
from gns3server.modules import IModule
from gns3server.config import Config
from .virtualbox_vm import VirtualBoxVM
from .virtualbox_error import VirtualBoxError
from .vboxwrapper_client import VboxWrapperClient
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
@ -60,25 +60,34 @@ class VirtualBox(IModule):
def __init__(self, name, *args, **kwargs):
# get the vboxwrapper location
config = Config.instance()
vbox_config = config.get_section_config(name.upper())
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
# look for iouyap in the current working directory and $PATH
for path in paths:
try:
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
self._vboxwrapper_path = os.path.join(path, "vboxwrapper")
break
except OSError:
continue
# get the vboxmanage location
self._vboxmanage_path = None
if sys.platform.startswith("win"):
if "VBOX_INSTALL_PATH" in os.environ:
self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
elif sys.platform.startswith("darwin"):
self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
else:
config = Config.instance()
vbox_config = config.get_section_config(name.upper())
self._vboxmanage_path = vbox_config.get("vboxmanage_path")
if not self._vboxmanage_path or not os.path.isfile(self._vboxmanage_path):
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for vboxmanage in the current working directory and $PATH
for path in paths:
try:
if "vboxmanage" in [s.lower() for s in os.listdir(path)] and os.access(os.path.join(path, "vboxmanage"), os.X_OK):
self._vboxmanage_path = os.path.join(path, "vboxmanage")
break
except OSError:
continue
if not self._vboxwrapper_path:
log.warning("vboxwrapper couldn't be found!")
elif not os.access(self._vboxwrapper_path, os.X_OK):
log.warning("vboxwrapper is not executable")
if not self._vboxmanage_path:
log.warning("vboxmanage couldn't be found!")
elif not os.access(self._vboxmanage_path, os.X_OK):
log.warning("vboxmanage is not executable")
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
@ -91,47 +100,11 @@ class VirtualBox(IModule):
self._allocated_udp_ports = []
self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500)
self._host = kwargs["host"]
self._host = vbox_config.get("host", kwargs["host"])
self._console_host = vbox_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._vboxmanager = None
self._vboxwrapper = None
def _start_vbox_service(self):
"""
Starts the VirtualBox backend.
vboxapi on Windows or vboxwrapper on other platforms.
"""
if sys.platform.startswith("win"):
import win32com.client
if win32com.client.gencache.is_readonly is True:
# dynamically generate the cache
# http://www.py2exe.org/index.cgi/IncludingTypelibs
# http://www.py2exe.org/index.cgi/UsingEnsureDispatch
win32com.client.gencache.is_readonly = False
#win32com.client.gencache.Rebuild()
win32com.client.gencache.GetGeneratePath()
try:
from .vboxapi_py3 import VirtualBoxManager
self._vboxmanager = VirtualBoxManager(None, None)
except Exception as e:
raise VirtualBoxError("Could not initialize the VirtualBox Manager: {}".format(e))
log.info("VirtualBox Manager has successful started: version is {} r{}".format(self._vboxmanager.vbox.version,
self._vboxmanager.vbox.revision))
else:
if not self._vboxwrapper_path:
raise VirtualBoxError("No vboxwrapper path has been configured")
if not os.path.isfile(self._vboxwrapper_path):
raise VirtualBoxError("vboxwrapper path doesn't exist {}".format(self._vboxwrapper_path))
self._vboxwrapper = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1")
#self._vboxwrapper.connect()
self._vboxwrapper.start()
def stop(self, signum=None):
"""
@ -143,10 +116,10 @@ class VirtualBox(IModule):
# delete all VirtualBox instances
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
vbox_instance.delete()
if self._vboxwrapper and self._vboxwrapper.started:
self._vboxwrapper.stop()
try:
vbox_instance.delete()
except VirtualBoxError:
continue
IModule.stop(self, signum) # this will stop the I/O loop
@ -154,9 +127,9 @@ class VirtualBox(IModule):
"""
Returns a VirtualBox VM instance.
:param vbox_id: VirtualBox device identifier
:param vbox_id: VirtualBox VM identifier
:returns: VBoxDevice instance
:returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
@ -184,9 +157,7 @@ class VirtualBox(IModule):
self._vbox_instances.clear()
self._allocated_udp_ports.clear()
if self._vboxwrapper and self._vboxwrapper.connected():
self._vboxwrapper.send("vboxwrapper reset")
self._working_dir = self._projects_dir
log.info("VirtualBox module has been reset")
@IModule.route("virtualbox.settings")
@ -196,7 +167,7 @@ class VirtualBox(IModule):
Optional request parameters:
- working_dir (path to a working directory)
- vboxwrapper_path (path to vboxwrapper)
- vboxmanage_path (path to vboxmanage)
- project_name
- console_start_port_range
- console_end_port_range
@ -231,10 +202,10 @@ class VirtualBox(IModule):
self._working_dir = new_working_dir
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "vm-{}".format(vbox_instance.id))
vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "{}".format(vbox_instance.name))
if "vboxwrapper_path" in request:
self._vboxwrapper_path = request["vboxwrapper_path"]
if "vboxmanage_path" in request:
self._vboxmanage_path = request["vboxmanage_path"]
if "console_start_port_range" in request and "console_end_port_range" in request:
self._console_start_port_range = request["console_start_port_range"]
@ -253,6 +224,8 @@ class VirtualBox(IModule):
Mandatory request parameters:
- name (VirtualBox VM name)
- vmname (VirtualBox VM name in VirtualBox)
- linked_clone (Flag to create a linked clone)
Optional request parameters:
- console (VirtualBox VM console port)
@ -271,22 +244,23 @@ class VirtualBox(IModule):
name = request["name"]
vmname = request["vmname"]
linked_clone = request["linked_clone"]
console = request.get("console")
vbox_id = request.get("vbox_id")
try:
if not self._vboxwrapper and not self._vboxmanager:
self._start_vbox_service()
if not self._vboxmanage_path or not os.path.exists(self._vboxmanage_path):
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
vbox_instance = VirtualBoxVM(self._vboxwrapper,
self._vboxmanager,
vbox_instance = VirtualBoxVM(self._vboxmanage_path,
name,
vmname,
linked_clone,
self._working_dir,
self._host,
vbox_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)
@ -632,7 +606,7 @@ class VirtualBox(IModule):
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VPCS instance identifier)
- id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
@ -667,7 +641,7 @@ class VirtualBox(IModule):
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
@ -708,7 +682,7 @@ class VirtualBox(IModule):
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
@ -737,6 +711,21 @@ class VirtualBox(IModule):
response = {"port_id": request["port_id"]}
self.send_response(response)
def _execute_vboxmanage(self, command):
"""
Executes VBoxManage and return its result.
:param command: command to execute (list)
:returns: VBoxManage output
"""
try:
result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=30)
except subprocess.SubprocessError as e:
raise VirtualBoxError("Could not execute VBoxManage {}".format(e))
return result.decode("utf-8")
@IModule.route("virtualbox.vm_list")
def vm_list(self, request):
"""
@ -747,22 +736,37 @@ class VirtualBox(IModule):
- List of VM names
"""
if not self._vboxwrapper and not self._vboxmanager:
self._start_vbox_service()
try:
if self._vboxwrapper:
vms = self._vboxwrapper.get_vm_list()
elif self._vboxmanager:
vms = []
machines = self._vboxmanager.getArray(self._vboxmanager.vbox, "machines")
for machine in range(len(machines)):
vms.append(machines[machine].name)
else:
self.send_custom_error("Vboxmanager hasn't been initialized!")
if request and "vboxmanage_path" in request:
vboxmanage_path = request["vboxmanage_path"]
else:
vboxmanage_path = self._vboxmanage_path
if not vboxmanage_path or not os.path.exists(vboxmanage_path):
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
command = [vboxmanage_path, "--nologo", "list", "vms"]
result = self._execute_vboxmanage(command)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"server": self._host,
"vms": vms}
vms = []
for line in result.splitlines():
vmname, uuid = line.rsplit(' ', 1)
vmname = vmname.strip('"')
if vmname == "<inaccessible>":
continue # ignore inaccessible VMs
try:
extra_data = self._execute_vboxmanage([vboxmanage_path, "getextradata", vmname, "GNS3/Clone"]).strip()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
if not extra_data == "Value: yes":
vms.append(vmname)
response = {"vms": vms}
self.send_response(response)
@IModule.route("virtualbox.echo")

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -1,476 +0,0 @@
# -*- 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/>.
# Parts of this code have been taken from Pyserial project (http://pyserial.sourceforge.net/) under Python license
import sys
import time
import threading
import socket
import select
if sys.platform.startswith("win"):
import win32pipe
import win32file
class PipeProxy(threading.Thread):
def __init__(self, name, pipe, host, port):
self.devname = name
self.pipe = pipe
self.host = host
self.port = port
self.server = None
self.reader_thread = None
self.use_thread = False
self._write_lock = threading.Lock()
self.clients = {}
self.timeout = 0.1
self.alive = True
if sys.platform.startswith("win"):
# we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select()
self.use_thread = True
try:
if self.host.__contains__(':'):
# IPv6 address support
self.server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((self.host, int(self.port)))
self.server.listen(5)
except socket.error as msg:
self.error("unable to create the socket server %s" % msg)
return
threading.Thread.__init__(self)
self.debug("initialized, waiting for clients on %s:%i..." % (self.host, self.port))
def error(self, msg):
sys.stderr.write("ERROR -> %s PIPE PROXY: %s\n" % (self.devname, msg))
def debug(self, msg):
sys.stdout.write("INFO -> %s PIPE PROXY: %s\n" % (self.devname, msg))
def run(self):
while True:
recv_list = [self.server.fileno()]
if not self.use_thread:
recv_list.append(self.pipe.fileno())
for client in self.clients.values():
if client.active:
recv_list.append(client.fileno)
else:
self.debug("lost client %s" % client.addrport())
try:
client.sock.close()
except:
pass
del self.clients[client.fileno]
try:
rlist, slist, elist = select.select(recv_list, [], [], self.timeout)
except select.error as err:
self.error("fatal select error %d:%s" % (err[0], err[1]))
return False
if not self.alive:
self.debug('Exiting ...')
return True
for sock_fileno in rlist:
if sock_fileno == self.server.fileno():
try:
sock, addr = self.server.accept()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.debug("new client %s:%s" % (addr[0], addr[1]))
except socket.error as err:
self.error("accept error %d:%s" % (err[0], err[1]))
continue
new_client = TelnetClient(sock, addr)
self.clients[new_client.fileno] = new_client
welcome_msg = "%s console is now available ... Press RETURN to get started.\r\n" % self.devname
sock.send(welcome_msg.encode('utf-8'))
if self.use_thread and not self.reader_thread:
self.reader_thread = threading.Thread(target=self.reader)
self.reader_thread.setDaemon(True)
self.reader_thread.setName('pipe->socket')
self.reader_thread.start()
elif not self.use_thread and sock_fileno == self.pipe.fileno():
data = self.read_from_pipe()
if not data:
self.debug("pipe has been closed!")
return False
for client in self.clients.values():
try:
client.send(data)
except:
self.debug(msg)
client.deactivate()
elif sock_fileno in self.clients:
try:
data = self.clients[sock_fileno].socket_recv()
# For some reason, windows likes to send "cr/lf" when you send a "cr".
# Strip that so we don't get a double prompt.
data = data.replace(b"\r\n", b"\n")
self.write_to_pipe(data)
except Exception as msg:
self.debug(msg)
self.clients[sock_fileno].deactivate()
def write_to_pipe(self, data):
if sys.platform.startswith('win'):
win32file.WriteFile(self.pipe, data)
else:
self.pipe.sendall(data)
def read_from_pipe(self):
if sys.platform.startswith('win'):
(read, num_avail, num_message) = win32pipe.PeekNamedPipe(self.pipe, 0)
if num_avail > 0:
(error_code, output) = win32file.ReadFile(self.pipe, num_avail, None)
return output
return ""
else:
return self.pipe.recv(1024)
def reader(self):
"""loop forever and copy pipe->socket"""
self.debug("reader thread started")
while self.alive:
try:
data = self.read_from_pipe()
if not data and not sys.platform.startswith('win'):
self.debug("pipe has been closed!")
break
self._write_lock.acquire()
try:
for client in self.clients.values():
client.send(data)
finally:
self._write_lock.release()
if sys.platform.startswith('win'):
# sleep every 10 ms
time.sleep(0.01)
except:
self.debug("pipe has been closed!")
break
self.debug("reader thread exited")
self.stop()
def stop(self):
"""Stop copying"""
if self.alive:
self.alive = False
for client in self.clients.values():
client.sock.close()
client.deactivate()
# telnet protocol characters
IAC = 255 # Interpret As Command
DONT = 254
DO = 253
WONT = 252
WILL = 251
IAC_DOUBLED = [IAC, IAC]
SE = 240 # Subnegotiation End
NOP = 241 # No Operation
DM = 242 # Data Mark
BRK = 243 # Break
IP = 244 # Interrupt process
AO = 245 # Abort output
AYT = 246 # Are You There
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # Go Ahead
SB = 250 # Subnegotiation Begin
# selected telnet options
ECHO = 1 # echo
SGA = 3 # suppress go ahead
LINEMODE = 34 # line mode
TERMTYPE = 24 # terminal type
# Telnet filter states
M_NORMAL = 0
M_IAC_SEEN = 1
M_NEGOTIATE = 2
# TelnetOption and TelnetSubnegotiation states
REQUESTED = 'REQUESTED'
ACTIVE = 'ACTIVE'
INACTIVE = 'INACTIVE'
REALLY_INACTIVE = 'REALLY_INACTIVE'
class TelnetOption(object):
"""Manage a single telnet option, keeps track of DO/DONT WILL/WONT."""
def __init__(self, connection, name, option, send_yes, send_no, ack_yes, ack_no, initial_state, activation_callback=None):
"""Init option.
:param connection: connection used to transmit answers
:param name: a readable name for debug outputs
:param send_yes: what to send when option is to be enabled.
:param send_no: what to send when option is to be disabled.
:param ack_yes: what to expect when remote agrees on option.
:param ack_no: what to expect when remote disagrees on option.
:param initial_state: options initialized with REQUESTED are tried to
be enabled on startup. use INACTIVE for all others.
"""
self.connection = connection
self.name = name
self.option = option
self.send_yes = send_yes
self.send_no = send_no
self.ack_yes = ack_yes
self.ack_no = ack_no
self.state = initial_state
self.active = False
self.activation_callback = activation_callback
def __repr__(self):
"""String for debug outputs"""
return "%s:%s(%s)" % (self.name, self.active, self.state)
def process_incoming(self, command):
"""A DO/DONT/WILL/WONT was received for this option, update state and
answer when needed."""
if command == self.ack_yes:
if self.state is REQUESTED:
self.state = ACTIVE
self.active = True
if self.activation_callback is not None:
self.activation_callback()
elif self.state is ACTIVE:
pass
elif self.state is INACTIVE:
self.state = ACTIVE
self.connection.telnetSendOption(self.send_yes, self.option)
self.active = True
if self.activation_callback is not None:
self.activation_callback()
elif self.state is REALLY_INACTIVE:
self.connection.telnetSendOption(self.send_no, self.option)
else:
raise ValueError('option in illegal state %r' % self)
elif command == self.ack_no:
if self.state is REQUESTED:
self.state = INACTIVE
self.active = False
elif self.state is ACTIVE:
self.state = INACTIVE
self.connection.telnetSendOption(self.send_no, self.option)
self.active = False
elif self.state is INACTIVE:
pass
elif self.state is REALLY_INACTIVE:
pass
else:
raise ValueError('option in illegal state %r' % self)
class TelnetClient(object):
"""
Represents a client connection via Telnet.
First argument is the socket discovered by the Telnet Server.
Second argument is the tuple (ip address, port number).
"""
def __init__(self, sock, addr_tup):
self.active = True # Turns False when the connection is lost
self.sock = sock # The connection's socket
self.fileno = sock.fileno() # The socket's file descriptor
self.address = addr_tup[0] # The client's remote TCP/IP address
self.port = addr_tup[1] # The client's remote port
# filter state machine
self.mode = M_NORMAL
self.suboption = None
self.telnet_command = None
# all supported telnet options
self._telnet_options = [
TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED),
TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE),
TelnetOption(self, 'LINEMODE', LINEMODE, DONT, DONT, WILL, WONT, REQUESTED),
TelnetOption(self, 'TERMTYPE', TERMTYPE, DO, DONT, WILL, WONT, REQUESTED),
]
for option in self._telnet_options:
if option.state is REQUESTED:
self.telnetSendOption(option.send_yes, option.option)
def telnetSendOption(self, action, option):
"""Send DO, DONT, WILL, WONT."""
self.sock.sendall(bytes([IAC, action, option]))
def escape(self, data):
""" All outgoing data has to be properly escaped, so that no IAC character
in the data stream messes up the Telnet state machine in the server.
"""
for byte in data:
if byte == IAC:
yield IAC
yield IAC
else:
yield byte
def filter(self, data):
""" handle a bunch of incoming bytes. this is a generator. it will yield
all characters not of interest for Telnet
"""
for byte in data:
if self.mode == M_NORMAL:
# interpret as command or as data
if byte == IAC:
self.mode = M_IAC_SEEN
else:
# store data in sub option buffer or pass it to our
# consumer depending on state
if self.suboption is not None:
self.suboption.append(byte)
else:
yield byte
elif self.mode == M_IAC_SEEN:
if byte == IAC:
# interpret as command doubled -> insert character
# itself
if self.suboption is not None:
self.suboption.append(byte)
else:
yield byte
self.mode = M_NORMAL
elif byte == SB:
# sub option start
self.suboption = bytearray()
self.mode = M_NORMAL
elif byte == SE:
# sub option end -> process it now
#self._telnetProcessSubnegotiation(bytes(self.suboption))
self.suboption = None
self.mode = M_NORMAL
elif byte in (DO, DONT, WILL, WONT):
# negotiation
self.telnet_command = byte
self.mode = M_NEGOTIATE
else:
# other telnet commands are ignored!
self.mode = M_NORMAL
elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
self._telnetNegotiateOption(self.telnet_command, byte)
self.mode = M_NORMAL
def _telnetNegotiateOption(self, command, option):
"""Process incoming DO, DONT, WILL, WONT."""
# check our registered telnet options and forward command to them
# they know themselves if they have to answer or not
known = False
for item in self._telnet_options:
# can have more than one match! as some options are duplicated for
# 'us' and 'them'
if item.option == option:
item.process_incoming(command)
known = True
if not known:
# handle unknown options
# only answer to positive requests and deny them
if command == WILL or command == DO:
self.telnetSendOption((command == WILL and DONT or WONT), option)
def send(self, data):
"""
Send data to the distant end.
"""
try:
self.sock.sendall(bytes(self.escape(data)))
except socket.error as ex:
self.active = False
raise Exception("socket.sendall() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport()))
def deactivate(self):
"""
Set the client to disconnect on the next server poll.
"""
self.active = False
def addrport(self):
"""
Return the DE's IP address and port number as a string.
"""
return "%s:%s" % (self.address, self.port)
def socket_recv(self):
"""
Called by TelnetServer when recv data is ready.
"""
try:
data = self.sock.recv(4096)
except socket.error as ex:
raise Exception("socket.recv() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport()))
## Did they close the connection?
size = len(data)
if size == 0:
raise Exception("connection closed by %s" % self.addrport())
return bytes(self.filter(data))
if __name__ == '__main__':
if sys.platform.startswith('win'):
import msvcrt
pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_3.8.2'
pipe = open(pipe_name, 'a+b')
pipe_proxy = PipeProxy("VBOX", msvcrt.get_osfhandle(pipe.fileno()), '127.0.0.1', 3900)
else:
try:
unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
#unix_socket.settimeout(0.1)
unix_socket.connect("/tmp/pipe_test")
except socket.error as err:
print("Socket error -> %d:%s" % (err[0], err[1]))
sys.exit(False)
pipe_proxy = PipeProxy('VBOX', unix_socket, '127.0.0.1', 3900)
pipe_proxy.setDaemon(True)
pipe_proxy.start()
pipe.proxy.stop()
pipe_proxy.join()

View File

@ -31,6 +31,10 @@ VBOX_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"linked_clone": {
"description": "either the VM is a linked clone or not",
"type": "boolean"
},
"vbox_id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
@ -82,9 +86,15 @@ VBOX_UPDATE_SCHEMA = {
"adapters": {
"description": "number of adapters",
"type": "integer",
"minimum": 0,
"minimum": 1,
"maximum": 36, # maximum given by the ICH9 chipset in VirtualBox
},
"adapter_start_index": {
"description": "adapter index from which to start using adapters",
"type": "integer",
"minimum": 0,
"maximum": 35, # maximum given by the ICH9 chipset in VirtualBox
},
"adapter_type": {
"description": "VirtualBox adapter type",
"type": "string",
@ -96,6 +106,10 @@ VBOX_UPDATE_SCHEMA = {
"maximum": 65535,
"type": "integer"
},
"enable_remote_console": {
"description": "enable the remote console",
"type": "boolean"
},
"headless": {
"description": "headless mode",
"type": "boolean"

View File

@ -0,0 +1,444 @@
# -*- 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/>.
import sys
import time
import threading
import socket
import select
import logging
log = logging.getLogger(__name__)
if sys.platform.startswith("win"):
import win32pipe
import win32file
class TelnetServer(threading.Thread):
"""
Mini Telnet Server.
:param vm_name: Virtual machine name
:param pipe_path: path to VM pipe (UNIX socket on Linux/UNIX, Named Pipe on Windows)
:param host: server host
:param port: server port
"""
def __init__(self, vm_name, pipe_path, host, port):
self._vm_name = vm_name
self._pipe = pipe_path
self._host = host
self._port = port
self._reader_thread = None
self._use_thread = False
self._write_lock = threading.Lock()
self._clients = {}
self._timeout = 1
self._alive = True
if sys.platform.startswith("win"):
# we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select()
self._use_thread = True
try:
if ":" in self._host:
# IPv6 address support
self._server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._server_socket.bind((self._host, self._port))
self._server_socket.listen(socket.SOMAXCONN)
except OSError as e:
log.critical("unable to create a server socket: {}".format(e))
return
threading.Thread.__init__(self)
log.info("Telnet server initialized, waiting for clients on {}:{}".format(self._host, self._port))
def run(self):
"""
Thread loop.
"""
while True:
recv_list = [self._server_socket.fileno()]
if not self._use_thread:
recv_list.append(self._pipe.fileno())
for client in self._clients.values():
if client.is_active():
recv_list.append(client.socket().fileno())
else:
del self._clients[client.socket().fileno()]
try:
client.socket().shutdown(socket.SHUT_RDWR)
except OSError as e:
log.warn("shutdown: {}".format(e))
client.socket().close()
break
try:
rlist, slist, elist = select.select(recv_list, [], [], self._timeout)
except OSError as e:
log.critical("fatal select error: {}".format(e))
return False
if not self._alive:
log.info("Telnet server for {} is exiting".format(self._vm_name))
return True
for sock_fileno in rlist:
if sock_fileno == self._server_socket.fileno():
try:
sock, addr = self._server_socket.accept()
host, port = addr
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
log.info("new client {}:{} has connected".format(host, port))
except OSError as e:
log.error("could not accept new client: {}".format(e))
continue
new_client = TelnetClient(self._vm_name, sock, host, port)
self._clients[sock.fileno()] = new_client
if self._use_thread and not self._reader_thread:
self._reader_thread = threading.Thread(target=self._reader, daemon=True)
self._reader_thread.start()
elif not self._use_thread and sock_fileno == self._pipe.fileno():
data = self._read_from_pipe()
if not data:
log.warning("pipe has been closed!")
return False
for client in self._clients.values():
try:
client.send(data)
except OSError as e:
log.debug(e)
client.deactivate()
elif sock_fileno in self._clients:
try:
data = self._clients[sock_fileno].socket_recv()
if not data:
continue
# For some reason, windows likes to send "cr/lf" when you send a "cr".
# Strip that so we don't get a double prompt.
data = data.replace(b"\r\n", b"\n")
self._write_to_pipe(data)
except Exception as msg:
log.info(msg)
self._clients[sock_fileno].deactivate()
def _write_to_pipe(self, data):
"""
Writes data to the pipe.
:param data: data to write
"""
if sys.platform.startswith('win'):
win32file.WriteFile(self._pipe, data)
else:
self._pipe.sendall(data)
def _read_from_pipe(self):
"""
Reads data from the pipe.
:returns: data
"""
if sys.platform.startswith('win'):
(read, num_avail, num_message) = win32pipe.PeekNamedPipe(self._pipe, 0)
if num_avail > 0:
(error_code, output) = win32file.ReadFile(self._pipe, num_avail, None)
return output
return b""
else:
return self._pipe.recv(1024)
def _reader(self):
"""
Loops forever and copy everything from the pipe to the socket.
"""
log.debug("reader thread has started")
while self._alive:
try:
data = self._read_from_pipe()
if not data and not sys.platform.startswith('win'):
log.debug("pipe has been closed! (no data)")
break
self._write_lock.acquire()
try:
for client in self._clients.values():
client.send(data)
finally:
self._write_lock.release()
if sys.platform.startswith('win'):
# sleep every 10 ms
time.sleep(0.01)
except Exception as e:
log.debug("pipe has been closed! {}".format(e))
break
log.debug("reader thread exited")
self.stop()
def stop(self):
"""
Stops the server.
"""
if self._alive:
self._alive = False
for client in self._clients.values():
client.socket().close()
client.deactivate()
# Mostly from https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
# Telnet Commands
SE = 240 # End of sub-negotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
# Telnet Options
BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode
class TelnetClient(object):
"""
Represents a Telnet client connection.
:param vm_name: VM name
:param sock: socket connection
:param host: IP of the Telnet client
:param port: port of the Telnet client
"""
def __init__(self, vm_name, sock, host, port):
self._active = True
self._sock = sock
self._host = host
self._port = port
sock.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
welcome_msg = "{} console is now available... Press RETURN to get started.\r\n".format(vm_name)
sock.send(welcome_msg.encode('utf-8'))
def is_active(self):
"""
Returns either the client is active or not.
:return: boolean
"""
return self._active
def socket(self):
"""
Returns the socket for this Telnet client.
:returns: socket instance.
"""
return self._sock
def send(self, data):
"""
Sends data to the remote end.
:param data: data to send
"""
try:
self._sock.send(data)
except OSError as e:
self._active = False
raise Exception("Socket send: {}".format(e))
def deactivate(self):
"""
Sets the client to disconnect on the next server poll.
"""
self._active = False
def socket_recv(self):
"""
Called by Telnet Server when data is ready.
"""
try:
buf = self._sock.recv(1024)
except BlockingIOError:
return None
except ConnectionResetError:
buf = b''
# is the connection closed?
if not buf:
raise Exception("connection closed by {}:{}".format(self._host, self._port))
# Process and remove any telnet commands from the buffer
if IAC in buf:
buf = self._IAC_parser(buf)
return buf
def _read_block(self, bufsize):
"""
Reads a block for data from the socket.
:param bufsize: size of the buffer
:returns: data read
"""
buf = self._sock.recv(1024, socket.MSG_WAITALL)
# If we don't get everything we were looking for then the
# client probably disconnected.
if len(buf) < bufsize:
raise Exception("connection closed by {}:{}".format(self._host, self._port))
return buf
def _IAC_parser(self, buf):
"""
Processes and removes any Telnet commands from the buffer.
:param buf: buffer
:returns: buffer minus Telnet commands
"""
skip_to = 0
while self._active:
# Locate an IAC to process
iac_loc = buf.find(IAC, skip_to)
if iac_loc < 0:
break
# Get the TELNET command
iac_cmd = bytearray([IAC])
try:
iac_cmd.append(buf[iac_loc + 1])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 1])
# Is this just a 2-byte TELNET command?
if iac_cmd[1] not in [WILL, WONT, DO, DONT]:
if iac_cmd[1] == AYT:
log.debug("Telnet server received Are-You-There (AYT)")
self._sock.send(b'\r\nYour Are-You-There received. I am here.\r\n')
elif iac_cmd[1] == IAC:
# It's data, not an IAC
iac_cmd.pop()
# This prevents the 0xff from being
# interrupted as yet another IAC
skip_to = iac_loc + 1
log.debug("Received IAC IAC")
elif iac_cmd[1] == NOP:
pass
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x}".format(*iac_cmd))
# This must be a 3-byte TELNET command
else:
try:
iac_cmd.append(buf[iac_loc + 2])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period.
if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]:
self._sock.send(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x} {2:#x}".format(*iac_cmd))
# Remove the entire TELNET command from the buffer
buf = buf.replace(iac_cmd, b'', 1)
# Return the new copy of the buffer, minus telnet commands
return buf
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
if sys.platform.startswith('win'):
import msvcrt
pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_4.7.1'
pipe = open(pipe_name, 'a+b')
telnet_server = TelnetServer("VBOX", msvcrt.get_osfhandle(pipe.fileno()), "127.0.0.1", 3900)
else:
pipe_name = "/tmp/pipe_test"
try:
unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
unix_socket.connect(pipe_name)
except OSError as e:
print("Could not connect to UNIX socket {}: {}".format(pipe_name, e))
sys.exit(False)
telnet_server = TelnetServer("VBOX", unix_socket, "127.0.0.1", 3900)
telnet_server.setDaemon(True)
telnet_server.start()
try:
telnet_server.join()
except KeyboardInterrupt:
telnet_server.stop()
telnet_server.join(timeout=3)

View File

@ -1,612 +0,0 @@
"""
Copyright (C) 2009-2012 Oracle Corporation
This file is part of VirtualBox Open Source Edition (OSE), as
available from http://www.virtualbox.org. This file is free software;
you can redistribute it and/or modify it under the terms of the GNU
General Public License (GPL) as published by the Free Software
Foundation, in version 2 as it comes in the "COPYING" file of the
VirtualBox OSE distribution. VirtualBox OSE is distributed in the
hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
"""
import sys,os
import traceback
# To set Python bitness on OSX use 'export VERSIONER_PYTHON_PREFER_32_BIT=yes'
VboxBinDir = os.environ.get("VBOX_PROGRAM_PATH", None)
VboxSdkDir = os.environ.get("VBOX_SDK_PATH", None)
if VboxBinDir is None:
# Will be set by the installer
VboxBinDir = "C:\\Program Files\\Oracle\\VirtualBox\\"
if VboxSdkDir is None:
# Will be set by the installer
VboxSdkDir = "C:\\Program Files\\Oracle\\VirtualBox\\sdk\\"
os.environ["VBOX_PROGRAM_PATH"] = VboxBinDir
os.environ["VBOX_SDK_PATH"] = VboxSdkDir
sys.path.append(VboxBinDir)
from .VirtualBox_constants import VirtualBoxReflectionInfo
class PerfCollector:
""" This class provides a wrapper over IPerformanceCollector in order to
get more 'pythonic' interface.
To begin collection of metrics use setup() method.
To get collected data use query() method.
It is possible to disable metric collection without changing collection
parameters with disable() method. The enable() method resumes metric
collection.
"""
def __init__(self, mgr, vbox):
""" Initializes the instance.
"""
self.mgr = mgr
self.isMscom = (mgr.type == 'MSCOM')
self.collector = vbox.performanceCollector
def setup(self, names, objects, period, nsamples):
""" Discards all previously collected values for the specified
metrics, sets the period of collection and the number of retained
samples, enables collection.
"""
self.collector.setupMetrics(names, objects, period, nsamples)
def enable(self, names, objects):
""" Resumes metric collection for the specified metrics.
"""
self.collector.enableMetrics(names, objects)
def disable(self, names, objects):
""" Suspends metric collection for the specified metrics.
"""
self.collector.disableMetrics(names, objects)
def query(self, names, objects):
""" Retrieves collected metric values as well as some auxiliary
information. Returns an array of dictionaries, one dictionary per
metric. Each dictionary contains the following entries:
'name': metric name
'object': managed object this metric associated with
'unit': unit of measurement
'scale': divide 'values' by this number to get float numbers
'values': collected data
'values_as_string': pre-processed values ready for 'print' statement
"""
# Get around the problem with input arrays returned in output
# parameters (see #3953) for MSCOM.
if self.isMscom:
(values, names, objects, names_out, objects_out, units, scales, sequence_numbers,
indices, lengths) = self.collector.queryMetricsData(names, objects)
else:
(values, names_out, objects_out, units, scales, sequence_numbers,
indices, lengths) = self.collector.queryMetricsData(names, objects)
out = []
for i in xrange(0, len(names_out)):
scale = int(scales[i])
if scale != 1:
fmt = '%.2f%s'
else:
fmt = '%d %s'
out.append({
'name':str(names_out[i]),
'object':str(objects_out[i]),
'unit':str(units[i]),
'scale':scale,
'values':[int(values[j]) for j in xrange(int(indices[i]), int(indices[i])+int(lengths[i]))],
'values_as_string':'['+', '.join([fmt % (int(values[j])/scale, units[i]) for j in xrange(int(indices[i]), int(indices[i])+int(lengths[i]))])+']'
})
return out
def ComifyName(name):
return name[0].capitalize()+name[1:]
_COMForward = { 'getattr' : None,
'setattr' : None}
def CustomGetAttr(self, attr):
# fastpath
if self.__class__.__dict__.get(attr) != None:
return self.__class__.__dict__.get(attr)
# try case-insensitivity workaround for class attributes (COM methods)
for k in self.__class__.__dict__.keys():
if k.lower() == attr.lower():
setattr(self.__class__, attr, self.__class__.__dict__[k])
return getattr(self, k)
try:
return _COMForward['getattr'](self,ComifyName(attr))
except AttributeError:
return _COMForward['getattr'](self,attr)
def CustomSetAttr(self, attr, value):
try:
return _COMForward['setattr'](self, ComifyName(attr), value)
except AttributeError:
return _COMForward['setattr'](self, attr, value)
class PlatformMSCOM:
# Class to fake access to constants in style of foo.bar.boo
class ConstantFake:
def __init__(self, parent, name):
self.__dict__['_parent'] = parent
self.__dict__['_name'] = name
self.__dict__['_consts'] = {}
try:
self.__dict__['_depth']=parent.__dict__['_depth']+1
except:
self.__dict__['_depth']=0
if self.__dict__['_depth'] > 4:
raise AttributeError
def __getattr__(self, attr):
import win32com
from win32com.client import constants
if attr.startswith("__"):
raise AttributeError
consts = self.__dict__['_consts']
fake = consts.get(attr, None)
if fake != None:
return fake
try:
name = self.__dict__['_name']
parent = self.__dict__['_parent']
while parent != None:
if parent._name is not None:
name = parent._name+'_'+name
parent = parent._parent
if name is not None:
name += "_" + attr
else:
name = attr
return win32com.client.constants.__getattr__(name)
except AttributeError as e:
fake = PlatformMSCOM.ConstantFake(self, attr)
consts[attr] = fake
return fake
class InterfacesWrapper:
def __init__(self):
self.__dict__['_rootFake'] = PlatformMSCOM.ConstantFake(None, None)
def __getattr__(self, a):
import win32com
from win32com.client import constants
if a.startswith("__"):
raise AttributeError
try:
return win32com.client.constants.__getattr__(a)
except AttributeError as e:
return self.__dict__['_rootFake'].__getattr__(a)
VBOX_TLB_GUID = '{46137EEC-703B-4FE5-AFD4-7C9BBBBA0259}'
VBOX_TLB_LCID = 0
VBOX_TLB_MAJOR = 1
VBOX_TLB_MINOR = 0
def __init__(self, params):
from win32com import universal
from win32com.client import gencache, DispatchBaseClass
from win32com.client import constants, getevents
import win32com
import pythoncom
import win32api
from win32con import DUPLICATE_SAME_ACCESS
from win32api import GetCurrentThread,GetCurrentThreadId,DuplicateHandle,GetCurrentProcess
import threading
pid = GetCurrentProcess()
self.tid = GetCurrentThreadId()
handle = DuplicateHandle(pid, GetCurrentThread(), pid, 0, 0, DUPLICATE_SAME_ACCESS)
self.handles = []
self.handles.append(handle)
_COMForward['getattr'] = DispatchBaseClass.__dict__['__getattr__']
DispatchBaseClass.__getattr__ = CustomGetAttr
_COMForward['setattr'] = DispatchBaseClass.__dict__['__setattr__']
DispatchBaseClass.__setattr__ = CustomSetAttr
win32com.client.gencache.EnsureDispatch('VirtualBox.Session')
win32com.client.gencache.EnsureDispatch('VirtualBox.VirtualBox')
self.oIntCv = threading.Condition()
self.fInterrupted = False;
def getSessionObject(self, vbox):
import win32com
from win32com.client import Dispatch
return win32com.client.Dispatch("VirtualBox.Session")
def getVirtualBox(self):
import win32com
from win32com.client import Dispatch
return win32com.client.Dispatch("VirtualBox.VirtualBox")
def getType(self):
return 'MSCOM'
def getRemote(self):
return False
def getArray(self, obj, field):
return obj.__getattr__(field)
def initPerThread(self):
import pythoncom
pythoncom.CoInitializeEx(0)
def deinitPerThread(self):
import pythoncom
pythoncom.CoUninitialize()
def createListener(self, impl, arg):
d = {}
d['BaseClass'] = impl
d['arg'] = arg
d['tlb_guid'] = PlatformMSCOM.VBOX_TLB_GUID
str = ""
str += "import win32com.server.util\n"
str += "import pythoncom\n"
str += "class ListenerImpl(BaseClass):\n"
str += " _com_interfaces_ = ['IEventListener']\n"
str += " _typelib_guid_ = tlb_guid\n"
str += " _typelib_version_ = 1, 0\n"
str += " _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER\n"
# Maybe we'd better implement Dynamic invoke policy, to be more flexible here
str += " _reg_policy_spec_ = 'win32com.server.policy.EventHandlerPolicy'\n"
# capitalized version of listener method
str += " HandleEvent=BaseClass.handleEvent\n"
str += " def __init__(self): BaseClass.__init__(self, arg)\n"
str += "result = win32com.server.util.wrap(ListenerImpl())\n"
exec(str,d,d)
return d['result']
def waitForEvents(self, timeout):
from win32api import GetCurrentThreadId
from win32event import INFINITE
from win32event import MsgWaitForMultipleObjects, \
QS_ALLINPUT, WAIT_TIMEOUT, WAIT_OBJECT_0
from pythoncom import PumpWaitingMessages
import types
if not isinstance(timeout, types.IntType):
raise TypeError("The timeout argument is not an integer")
if (self.tid != GetCurrentThreadId()):
raise Exception("wait for events from the same thread you inited!")
if timeout < 0: cMsTimeout = INFINITE
else: cMsTimeout = timeout
rc = MsgWaitForMultipleObjects(self.handles, 0, cMsTimeout, QS_ALLINPUT)
if rc >= WAIT_OBJECT_0 and rc < WAIT_OBJECT_0+len(self.handles):
# is it possible?
rc = 2;
elif rc==WAIT_OBJECT_0 + len(self.handles):
# Waiting messages
PumpWaitingMessages()
rc = 0;
else:
# Timeout
rc = 1;
# check for interruption
self.oIntCv.acquire()
if self.fInterrupted:
self.fInterrupted = False
rc = 1;
self.oIntCv.release()
return rc;
def interruptWaitEvents(self):
"""
Basically a python implementation of EventQueue::postEvent().
The magic value must be in sync with the C++ implementation or this
won't work.
Note that because of this method we cannot easily make use of a
non-visible Window to handle the message like we would like to do.
"""
from win32api import PostThreadMessage
from win32con import WM_USER
self.oIntCv.acquire()
self.fInterrupted = True
self.oIntCv.release()
try:
PostThreadMessage(self.tid, WM_USER, None, 0xf241b819)
except:
return False;
return True;
def deinit(self):
import pythoncom
from win32file import CloseHandle
for h in self.handles:
if h is not None:
CloseHandle(h)
self.handles = None
pythoncom.CoUninitialize()
pass
def queryInterface(self, obj, klazzName):
from win32com.client import CastTo
return CastTo(obj, klazzName)
class PlatformXPCOM:
def __init__(self, params):
sys.path.append(VboxSdkDir+'/bindings/xpcom/python/')
import xpcom.vboxxpcom
import xpcom
import xpcom.components
def getSessionObject(self, vbox):
import xpcom.components
return xpcom.components.classes["@virtualbox.org/Session;1"].createInstance()
def getVirtualBox(self):
import xpcom.components
return xpcom.components.classes["@virtualbox.org/VirtualBox;1"].createInstance()
def getType(self):
return 'XPCOM'
def getRemote(self):
return False
def getArray(self, obj, field):
return obj.__getattr__('get'+ComifyName(field))()
def initPerThread(self):
import xpcom
xpcom._xpcom.AttachThread()
def deinitPerThread(self):
import xpcom
xpcom._xpcom.DetachThread()
def createListener(self, impl, arg):
d = {}
d['BaseClass'] = impl
d['arg'] = arg
str = ""
str += "import xpcom.components\n"
str += "class ListenerImpl(BaseClass):\n"
str += " _com_interfaces_ = xpcom.components.interfaces.IEventListener\n"
str += " def __init__(self): BaseClass.__init__(self, arg)\n"
str += "result = ListenerImpl()\n"
exec(str,d,d)
return d['result']
def waitForEvents(self, timeout):
import xpcom
return xpcom._xpcom.WaitForEvents(timeout)
def interruptWaitEvents(self):
import xpcom
return xpcom._xpcom.InterruptWait()
def deinit(self):
import xpcom
xpcom._xpcom.DeinitCOM()
def queryInterface(self, obj, klazzName):
import xpcom.components
return obj.queryInterface(getattr(xpcom.components.interfaces, klazzName))
class PlatformWEBSERVICE:
def __init__(self, params):
sys.path.append(os.path.join(VboxSdkDir,'bindings', 'webservice', 'python', 'lib'))
#import VirtualBox_services
import VirtualBox_wrappers
from VirtualBox_wrappers import IWebsessionManager2
if params is not None:
self.user = params.get("user", "")
self.password = params.get("password", "")
self.url = params.get("url", "")
else:
self.user = ""
self.password = ""
self.url = None
self.vbox = None
def getSessionObject(self, vbox):
return self.wsmgr.getSessionObject(vbox)
def getVirtualBox(self):
return self.connect(self.url, self.user, self.password)
def connect(self, url, user, passwd):
if self.vbox is not None:
self.disconnect()
from VirtualBox_wrappers import IWebsessionManager2
if url is None:
url = ""
self.url = url
if user is None:
user = ""
self.user = user
if passwd is None:
passwd = ""
self.password = passwd
self.wsmgr = IWebsessionManager2(self.url)
self.vbox = self.wsmgr.logon(self.user, self.password)
if not self.vbox.handle:
raise Exception("cannot connect to '"+self.url+"' as '"+self.user+"'")
return self.vbox
def disconnect(self):
if self.vbox is not None and self.wsmgr is not None:
self.wsmgr.logoff(self.vbox)
self.vbox = None
self.wsmgr = None
def getType(self):
return 'WEBSERVICE'
def getRemote(self):
return True
def getArray(self, obj, field):
return obj.__getattr__(field)
def initPerThread(self):
pass
def deinitPerThread(self):
pass
def createListener(self, impl, arg):
raise Exception("no active listeners for webservices")
def waitForEvents(self, timeout):
# Webservices cannot do that yet
return 2;
def interruptWaitEvents(self, timeout):
# Webservices cannot do that yet
return False;
def deinit(self):
try:
disconnect()
except:
pass
def queryInterface(self, obj, klazzName):
d = {}
d['obj'] = obj
str = ""
str += "from VirtualBox_wrappers import "+klazzName+"\n"
str += "result = "+klazzName+"(obj.mgr,obj.handle)\n"
# wrong, need to test if class indeed implements this interface
exec(str,d,d)
return d['result']
class SessionManager:
def __init__(self, mgr):
self.mgr = mgr
def getSessionObject(self, vbox):
return self.mgr.platform.getSessionObject(vbox)
class VirtualBoxManager:
def __init__(self, style, platparams):
if style is None:
if sys.platform == 'win32':
style = "MSCOM"
else:
style = "XPCOM"
exec("self.platform = Platform"+style+"(platparams)")
# for webservices, enums are symbolic
self.constants = VirtualBoxReflectionInfo(style == "WEBSERVICE")
self.type = self.platform.getType()
self.remote = self.platform.getRemote()
self.style = style
self.mgr = SessionManager(self)
try:
self.vbox = self.platform.getVirtualBox()
except NameError as ne:
print("Installation problem: check that appropriate libs in place")
traceback.print_exc()
raise ne
except Exception as e:
print("init exception: ",e)
traceback.print_exc()
if self.remote:
self.vbox = None
else:
raise e
def getArray(self, obj, field):
return self.platform.getArray(obj, field)
def getVirtualBox(self):
return self.platform.getVirtualBox()
def __del__(self):
self.deinit()
def deinit(self):
if hasattr(self, "vbox"):
del self.vbox
self.vbox = None
if hasattr(self, "platform"):
self.platform.deinit()
self.platform = None
def initPerThread(self):
self.platform.initPerThread()
def openMachineSession(self, mach, permitSharing = True):
session = self.mgr.getSessionObject(self.vbox)
if permitSharing:
type = self.constants.LockType_Shared
else:
type = self.constants.LockType_Write
mach.lockMachine(session, type)
return session
def closeMachineSession(self, session):
if session is not None:
session.unlockMachine()
def deinitPerThread(self):
self.platform.deinitPerThread()
def createListener(self, impl, arg = None):
return self.platform.createListener(impl, arg)
def waitForEvents(self, timeout):
"""
Wait for events to arrive and process them.
The timeout is in milliseconds. A negative value means waiting for
ever, while 0 does not wait at all.
Returns 0 if events was processed.
Returns 1 if timed out or interrupted in some way.
Returns 2 on error (like not supported for web services).
Raises an exception if the calling thread is not the main thread (the one
that initialized VirtualBoxManager) or if the time isn't an integer.
"""
return self.platform.waitForEvents(timeout)
def interruptWaitEvents(self):
"""
Interrupt a waitForEvents call.
This is normally called from a worker thread.
Returns True on success, False on failure.
"""
return self.platform.interruptWaitEvents()
def getPerfCollector(self, vbox):
return PerfCollector(self, vbox)
def getBinDir(self):
global VboxBinDir
return VboxBinDir
def getSdkDir(self):
global VboxSdkDir
return VboxSdkDir
def queryInterface(self, obj, klazzName):
return self.platform.queryInterface(obj, klazzName)

View File

@ -1,409 +0,0 @@
# -*- 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/>.
"""
Client to VirtualBox wrapper.
"""
import os
import time
import subprocess
import tempfile
import socket
import re
from ..attic import wait_socket_is_ready
from .virtualbox_error import VirtualBoxError
import logging
log = logging.getLogger(__name__)
class VboxWrapperClient(object):
"""
VirtualBox Wrapper client.
:param path: path to VirtualBox wrapper executable
:param working_dir: working directory
:param port: port
:param host: host/address
"""
# Used to parse the VirtualBox wrapper response codes
error_re = re.compile(r"""^2[0-9]{2}-""")
success_re = re.compile(r"""^1[0-9]{2}\s{1}""")
def __init__(self, path, working_dir, host, port=11525, timeout=30.0):
self._path = path
self._command = []
self._process = None
self._working_dir = working_dir
self._stdout_file = ""
self._started = False
self._host = host
self._port = port
self._timeout = timeout
self._socket = None
@property
def started(self):
"""
Returns either VirtualBox wrapper has been started or not.
:returns: boolean
"""
return self._started
@property
def path(self):
"""
Returns the path to the VirtualBox wrapper executable.
:returns: path to VirtualBox wrapper
"""
return self._path
@path.setter
def path(self, path):
"""
Sets the path to the VirtualBox wrapper executable.
:param path: path to VirtualBox wrapper
"""
self._path = path
@property
def port(self):
"""
Returns the port used to start the VirtualBox wrapper.
:returns: port number (integer)
"""
return self._port
@port.setter
def port(self, port):
"""
Sets the port used to start the VirtualBox wrapper.
:param port: port number (integer)
"""
self._port = port
@property
def host(self):
"""
Returns the host (binding) used to start the VirtualBox wrapper.
:returns: host/address (string)
"""
return self._host
@host.setter
def host(self, host):
"""
Sets the host (binding) used to start the VirtualBox wrapper.
:param host: host/address (string)
"""
self._host = host
def start(self):
"""
Starts the VirtualBox wrapper process.
"""
self._command = self._build_command()
try:
log.info("starting VirtualBox wrapper: {}".format(self._command))
with tempfile.NamedTemporaryFile(delete=False) as fd:
self._stdout_file = fd.name
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
self._process = subprocess.Popen(self._command,
stdout=fd,
stderr=subprocess.STDOUT,
cwd=self._working_dir)
log.info("VirtualBox wrapper started PID={}".format(self._process.pid))
self.wait_for_vboxwrapper(self._host, self._port)
self.connect()
self._started = True
except OSError as e:
log.error("could not start VirtualBox wrapper: {}".format(e))
raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e))
def wait_for_vboxwrapper(self, host, port):
"""
Waits for vboxwrapper to be started (accepting a socket connection)
:param host: host/address to connect to the vboxwrapper
:param port: port to connect to the vboxwrapper
"""
begin = time.time()
# wait for the socket for a maximum of 10 seconds.
connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0)
if not connection_success:
raise VirtualBoxError("Couldn't connect to vboxwrapper on {}:{} :{}".format(host, port,
last_exception))
else:
log.info("vboxwrapper server ready after {:.4f} seconds".format(time.time() - begin))
def stop(self):
"""
Stops the VirtualBox wrapper process.
"""
if self.connected():
try:
self.send("vboxwrapper stop")
except VirtualBoxError:
pass
if self._socket:
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
self._socket = None
if self.is_running():
log.info("stopping VirtualBox wrapper PID={}".format(self._process.pid))
try:
# give some time for the VirtualBox wrapper to properly stop.
time.sleep(0.01)
self._process.terminate()
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() is None:
log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid))
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 VirtualBox wrapper log file: {}".format(e))
self._started = False
def read_stdout(self):
"""
Reads the standard output of the VirtualBox wrapper 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.poll() is None:
return True
return False
def _build_command(self):
"""
Command to start the VirtualBox wrapper process.
(to be passed to subprocess.Popen())
"""
command = [self._path]
if self._host != "0.0.0.0" and self._host != "::":
command.extend(["-l", self._host, "-p", str(self._port)])
else:
command.extend(["-p", str(self._port)])
return command
def connect(self):
"""
Connects to the VirtualBox wrapper.
"""
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if self._host == "0.0.0.0":
host = "127.0.0.1"
elif self._host == "::":
host = "::1"
else:
host = self._host
try:
self._socket = socket.create_connection((host, self._port), self._timeout)
except OSError as e:
raise VirtualBoxError("Could not connect to the VirtualBox wrapper: {}".format(e))
def connected(self):
"""
Returns either the client is connected to vboxwrapper or not.
:return: boolean
"""
if self._socket:
return True
return False
def reset(self):
"""
Resets the VirtualBox wrapper (used to get an empty configuration).
"""
pass
@property
def working_dir(self):
"""
Returns current working directory
:returns: path to the working directory
"""
return self._working_dir
@working_dir.setter
def working_dir(self, working_dir):
"""
Sets the working directory for the VirtualBox wrapper.
:param working_dir: path to the working directory
"""
# encase working_dir in quotes to protect spaces in the path
#self.send("hypervisor working_dir {}".format('"' + working_dir + '"'))
self._working_dir = working_dir
log.debug("working directory set to {}".format(self._working_dir))
@property
def socket(self):
"""
Returns the current socket used to communicate with the VirtualBox wrapper.
:returns: socket instance
"""
assert self._socket
return self._socket
def get_vm_list(self):
"""
Returns the list of all VirtualBox VMs.
:returns: list of VM names
"""
return self.send('vbox vm_list')
def send(self, command):
"""
Sends commands to the VirtualBox wrapper.
:param command: a VirtualBox wrapper command
:returns: results as a list
"""
# VirtualBox wrapper responses are of the form:
# 1xx yyyyyy\r\n
# 1xx yyyyyy\r\n
# ...
# 100-yyyy\r\n
# or
# 2xx-yyyy\r\n
#
# Where 1xx is a code from 100-199 for a success or 200-299 for an error
# The result might be multiple lines and might be less than the buffer size
# but still have more data. The only thing we know for sure is the last line
# will begin with '100-' or a '2xx-' and end with '\r\n'
if not self._socket:
raise VirtualBoxError("Not connected")
try:
command = command.strip() + '\n'
log.debug("sending {}".format(command))
self.socket.sendall(command.encode('utf-8'))
except OSError as e:
self._socket = None
raise VirtualBoxError("Lost communication with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
# Now retrieve the result
data = []
buf = ''
while True:
try:
chunk = self.socket.recv(1024)
buf += chunk.decode("utf-8")
except OSError as e:
self._socket = None
raise VirtualBoxError("Communication timed out with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
# If the buffer doesn't end in '\n' then we can't be done
try:
if buf[-1] != '\n':
continue
except IndexError:
self._socket = None
raise VirtualBoxError("Could not communicate with {host}:{port}"
.format(host=self._host, port=self._port))
data += buf.split('\r\n')
if data[-1] == '':
data.pop()
buf = ''
if len(data) == 0:
raise VirtualBoxError("no data returned from {host}:{port}"
.format(host=self._host, port=self._port))
# Does it contain an error code?
if self.error_re.search(data[-1]):
raise VirtualBoxError(data[-1][4:])
# Or does the last line begin with '100-'? Then we are done!
if data[-1][:4] == '100-':
data[-1] = data[-1][4:]
if data[-1] == 'OK':
data.pop()
break
# Remove success responses codes
for index in range(len(data)):
if self.success_re.search(data[index]):
data[index] = data[index][4:]
log.debug("returned result {}".format(data))
return data

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,7 @@ class VPCS(IModule):
vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs_path")
if not self._vpcs or not os.path.isfile(self._vpcs):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for VPCS in the current working directory and $PATH
for path in paths:
try:
@ -81,12 +81,13 @@ class VPCS(IModule):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {}
self._console_start_port_range = vpcs_config.get("console_start_port_range", 4512)
self._console_start_port_range = vpcs_config.get("console_start_port_range", 4501)
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
self._allocated_udp_ports = []
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 40001)
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 40512)
self._host = kwargs["host"]
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
self._host = vpcs_config.get("host", kwargs["host"])
self._console_host = vpcs_config.get("console_host", kwargs["console_host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
@ -139,6 +140,7 @@ class VPCS(IModule):
self._vpcs_instances.clear()
self._allocated_udp_ports.clear()
self._working_dir = self._projects_dir
log.info("VPCS module has been reset")
@IModule.route("vpcs.settings")
@ -237,9 +239,9 @@ class VPCS(IModule):
vpcs_instance = VPCSDevice(name,
self._vpcs,
self._working_dir,
self._host,
vpcs_id,
console,
self._console_host,
self._console_start_port_range,
self._console_end_port_range)

View File

@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
class NIO_TAP(object):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -22,7 +22,7 @@ Interface for UDP NIOs.
class NIO_UDP(object):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -45,9 +45,9 @@ class VPCSDevice(object):
:param name: name of this VPCS device
:param path: path to VPCS executable
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param vpcs_id: VPCS instance ID
: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
"""
@ -59,9 +59,9 @@ class VPCSDevice(object):
name,
path,
working_dir,
host="127.0.0.1",
vpcs_id=None,
console=None,
console_host="0.0.0.0",
console_start_port_range=4512,
console_end_port_range=5000):
@ -89,7 +89,7 @@ class VPCSDevice(object):
self._path = path
self._console = console
self._working_dir = None
self._host = host
self._console_host = console_host
self._command = []
self._process = None
self._vpcs_stdout_file = ""
@ -114,7 +114,7 @@ class VPCSDevice(object):
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
self._console_host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise VPCSError(e)
@ -338,16 +338,15 @@ class VPCSDevice(object):
"""
try:
output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir)
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)
print(version)
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.CalledProcessError) as e:
except subprocess.SubprocessError as e:
raise VPCSError("Error while looking for the VPCS version: {}".format(e))
def start(self):
@ -387,7 +386,7 @@ class VPCSDevice(object):
creationflags=flags)
log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
self._started = True
except OSError as e:
except 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))

View File

@ -33,12 +33,16 @@ import tornado.ioloop
import tornado.web
import tornado.autoreload
import pkg_resources
import ipaddress
import base64
import uuid
from pkg_resources import parse_version
from .config import Config
from .handlers.jsonrpc_websocket import JSONRPCWebSocket
from .handlers.version_handler import VersionHandler
from .handlers.file_upload_handler import FileUploadHandler
from .handlers.auth_handler import LoginHandler
from .builtins.server_version import server_version
from .builtins.interfaces import interfaces
from .modules import MODULES
@ -51,15 +55,24 @@ class Server(object):
# built-in handlers
handlers = [(r"/version", VersionHandler),
(r"/upload", FileUploadHandler)]
(r"/upload", FileUploadHandler),
(r"/login", LoginHandler)]
def __init__(self, host, port, ipc=False):
def __init__(self, host, port, ipc, console_bind_to_any):
self._host = host
self._port = port
self._router = None
self._stream = None
if console_bind_to_any:
if ipaddress.ip_address(self._host).version == 6:
self._console_host = "::"
else:
self._console_host = "0.0.0.0"
else:
self._console_host = self._host
if ipc:
self._zmq_port = 0 # this forces to use IPC for communications with the ZeroMQ server
else:
@ -127,6 +140,7 @@ class Server(object):
"127.0.0.1", # ZeroMQ server address
self._zmq_port, # ZeroMQ server port
host=self._host, # server host address
console_host=self._console_host,
projects_dir=self._projects_dir,
temp_dir=self._temp_dir)
@ -141,6 +155,35 @@ class Server(object):
Starts the Tornado web server and ZeroMQ server.
"""
settings = {
"debug":True,
"cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes),
"login_url": "/login",
}
ssl_options = {}
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
cloud_settings = {
"required_user" : cloud_config['WEB_USERNAME'],
"required_pass" : cloud_config['WEB_PASSWORD'],
}
settings.update(cloud_settings)
if cloud_config["SSL_ENABLED"] == "yes":
ssl_options = {
"certfile" : cloud_config["SSL_CRT"],
"keyfile" : cloud_config["SSL_KEY"],
}
log.info("Certs found - starting in SSL mode")
except KeyError:
log.info("Missing cloud.conf - disabling HTTP auth and SSL")
router = self._create_zmq_router()
# Add our JSON-RPC Websocket handler to Tornado
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
@ -150,17 +193,21 @@ class Server(object):
templates_dir = pkg_resources.resource_filename("gns3server", "templates")
tornado_app = tornado.web.Application(self.handlers,
template_path=templates_dir,
debug=True) # FIXME: debug mode!
**settings) # FIXME: debug mode!
try:
print("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(self._host,
self._port,
tornado.version,
zmq.__version__,
zmq.zmq_version()))
user_log = logging.getLogger('user_facing')
user_log.info("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(
self._host, self._port, tornado.version, zmq.__version__, zmq.zmq_version()))
kwargs = {"address": self._host}
if ssl_options:
kwargs["ssl_options"] = ssl_options
if parse_version(tornado.version) >= parse_version("3.1"):
kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit
tornado_app.listen(self._port, **kwargs)
except OSError as e:
if e.errno == errno.EADDRINUSE: # socket already in use
@ -191,7 +238,7 @@ class Server(object):
try:
ioloop.start()
except (KeyboardInterrupt, SystemExit):
print("\nExiting...")
log.info("\nExiting...")
self._cleanup()
def _create_zmq_router(self):

254
gns3server/start_server.py Normal file
View File

@ -0,0 +1,254 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Startup script for a GNS3 Server Cloud Instance. It generates certificates,
config files and usernames before finally starting the gns3server process
on the instance.
"""
import os
import sys
import configparser
import getopt
import datetime
import signal
from logging.handlers import *
from os.path import expanduser
from gns3server.config import Config
import ast
import subprocess
import uuid
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
LOG_NAME = "gns3-startup"
log = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-i --ip The ip address of the server, for cert generation
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--data Python dict of data to be written to the config file:
" { 'gns3' : 'Is AWESOME' } "
""" % (SCRIPT_NAME)
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Passed in sys.argv
"""
short_args = "dvh"
long_args = ("debug",
"ip=",
"verbose",
"help",
"data=",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {'debug': False, 'verbose': True, 'data': None}
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
for opt, val in opts:
if opt in ("-h", "--help"):
print(usage)
sys.exit(0)
elif opt in ("-d", "--debug"):
cmd_line_option_list["debug"] = True
elif opt in ("--ip",):
cmd_line_option_list["ip"] = val
elif opt in ("-v", "--verbose"):
cmd_line_option_list["verbose"] = True
elif opt in ("--data",):
cmd_line_option_list["data"] = ast.literal_eval(val)
return cmd_line_option_list
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] is True:
log_level_console = logging.INFO
if cmd_options['debug'] is True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_handler = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_handler.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_handler)
return log
def _generate_certs(options):
"""
Generate a self-signed certificate for SSL-enabling the WebSocket
connection. The certificate is sent back to the client so it can
verify the authenticity of the server.
:return: A 2-tuple of strings containing (server_key, server_cert)
"""
cmd = ["{}/cert_utils/create_cert.sh".format(SCRIPT_PATH), options['ip']]
log.debug("Generating certs with cmd: {}".format(' '.join(cmd)))
output_raw = subprocess.check_output(cmd, shell=False,
stderr=subprocess.STDOUT)
output_str = output_raw.decode("utf-8")
output = output_str.strip().split("\n")
log.debug(output)
return (output[-2], output[-1])
def _start_gns3server():
"""
Start up the gns3 server.
:return: None
"""
cmd = 'gns3server --quiet > /tmp/gns3.log 2>&1 &'
log.info("Starting gns3server with cmd {}".format(cmd))
os.system(cmd)
def main():
global log
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
sys.exit(0)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
client_data = {}
config = Config.instance()
cfg = config.list_cloud_config_file()
cfg_path = os.path.dirname(cfg)
try:
os.makedirs(cfg_path)
except FileExistsError:
pass
(server_key, server_crt) = _generate_certs(options)
cloud_config = configparser.ConfigParser()
cloud_config['CLOUD_SERVER'] = {}
if options['data']:
cloud_config['CLOUD_SERVER'] = options['data']
cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key
cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt
cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'no'
cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8]
cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = str(uuid.uuid4()).upper()[0:8]
with open(cfg, 'w') as cloud_config_file:
cloud_config.write(cloud_config_file)
_start_gns3server()
with open(server_crt, 'r') as cert_file:
cert_data = cert_file.readlines()
cert_file.close()
# Return a stringified dictionary on stdout. The gui captures this to get
# things like the server cert.
client_data['SSL_CRT_FILE'] = server_crt
client_data['SSL_CRT'] = cert_data
client_data['WEB_USERNAME'] = cloud_config['CLOUD_SERVER']['WEB_USERNAME']
client_data['WEB_PASSWORD'] = cloud_config['CLOUD_SERVER']['WEB_PASSWORD']
print(client_data)
return 0
if __name__ == "__main__":
result = main()
sys.exit(result)

View File

@ -13,10 +13,8 @@ File: <input type="file" name="file" />
</form>
{%if items%}
<h3>Files on {{host}}</h3>
<ul>
{%for item in items%}
<li>{{path}}/{{item}}</a></li>
<p>{{path}}/{{item}}</a></p>
{%end%}
{%end%}
</ul>
</body>

View File

@ -23,5 +23,5 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "1.0beta1"
__version_info__ = (1, 0, 0, -99)
__version__ = "1.2.1"
__version_info__ = (1, 2, 1, 0)

View File

@ -1,5 +1,9 @@
netifaces
tornado
tornado==3.2.2
pyzmq
netifaces-py3
jsonschema
pycurl
python-dateutil
apache-libcloud
requests

46
scripts/ws_client.py Normal file
View File

@ -0,0 +1,46 @@
# -*- 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 ws4py.client.threadedclient import WebSocketClient
class WSClient(WebSocketClient):
def opened(self):
print("Connection successful with {}:{}".format(self.host, self.port))
self.send('{"jsonrpc": 2.0, "method": "dynamips.settings", "params": {"path": "/usr/local/bin/dynamips", "allocate_hypervisor_per_device": true, "working_dir": "/tmp/gns3-1b4grwm3-files", "udp_end_port_range": 20000, "sparse_memory_support": true, "allocate_hypervisor_per_ios_image": true, "aux_start_port_range": 2501, "use_local_server": true, "hypervisor_end_port_range": 7700, "aux_end_port_range": 3000, "mmap_support": true, "console_start_port_range": 2001, "console_end_port_range": 2500, "hypervisor_start_port_range": 7200, "ghost_ios_support": true, "memory_usage_limit_per_hypervisor": 1024, "jit_sharing_support": false, "udp_start_port_range": 10001}}')
self.send('{"jsonrpc": 2.0, "method": "dynamips.vm.create", "id": "e8caf5be-de3d-40dd-80b9-ab6df8029570", "params": {"image": "/home/grossmj/GNS3/images/IOS/c3725-advipservicesk9-mz.124-15.T14.image", "name": "R1", "platform": "c3725", "ram": 256}}')
def closed(self, code, reason=None):
print("Closed down. Code: {} Reason: {}".format(code, reason))
def received_message(self, m):
print(m)
if len(m) == 175:
self.close(reason='Bye bye')
if __name__ == '__main__':
try:
ws = WSClient('ws://localhost:8000/', protocols=['http-only', 'chat'])
ws.connect()
ws.run_forever()
except KeyboardInterrupt:
ws.close()

View File

@ -20,7 +20,7 @@ from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
class Tox(TestCommand):
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
@ -29,8 +29,8 @@ class Tox(TestCommand):
def run_tests(self):
#import here, cause outside the eggs aren't loaded
import tox
errcode = tox.cmdline(self.test_args)
import pytest
errcode = pytest.main(self.test_args)
sys.exit(errcode)
setup(
@ -38,8 +38,8 @@ setup(
version=__import__("gns3server").__version__,
url="http://github.com/GNS3/gns3-server",
license="GNU General Public License v3 (GPLv3)",
tests_require=["tox"],
cmdclass={"test": Tox},
tests_require=["pytest"],
cmdclass={"test": PyTest},
author="Jeremy Grossmann",
author_email="package-maintainer@gns3.net",
description="GNS3 server to asynchronously manage emulators",
@ -47,11 +47,14 @@ setup(
install_requires=[
"tornado>=3.1",
"pyzmq>=14.0.0",
"jsonschema>=2.3.0"
"jsonschema>=2.3.0",
"apache-libcloud>=0.14.1",
"requests",
],
entry_points={
"console_scripts": [
"gns3server = gns3server.main:main",
"gns3dms = gns3dms.main:main",
]
},
packages=find_packages(),
@ -59,7 +62,7 @@ setup(
include_package_data=True,
platforms="any",
classifiers=[
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Information Technology",
"Topic :: System :: Networking",

View File

@ -6,10 +6,9 @@ import os
@pytest.fixture(scope="module")
def hypervisor(request):
cwd = os.path.dirname(os.path.abspath(__file__))
dynamips_path = os.path.join(cwd, "dynamips.stable")
dynamips_path = '/usr/bin/dynamips'
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
hypervisor = manager.start_new_hypervisor()
def stop():

View File

@ -1,6 +1,5 @@
from gns3server.modules.dynamips import Hypervisor
import time
import os
def test_is_started(hypervisor):
@ -10,7 +9,7 @@ def test_is_started(hypervisor):
def test_port(hypervisor):
assert hypervisor.port == 9000
assert hypervisor.port == 7200
def test_host(hypervisor):
@ -25,8 +24,7 @@ def test_working_dir(hypervisor):
def test_path(hypervisor):
cwd = os.path.dirname(os.path.abspath(__file__))
dynamips_path = os.path.join(cwd, "dynamips.stable")
dynamips_path = '/usr/bin/dynamips'
assert hypervisor.path == dynamips_path
@ -34,11 +32,10 @@ def test_stdout():
# try to launch Dynamips on the same port
# this will fail so that we can read its stdout/stderr
cwd = os.path.dirname(os.path.abspath(__file__))
dynamips_path = os.path.join(cwd, "dynamips.stable")
hypervisor = Hypervisor(dynamips_path, "/tmp", "172.0.0.1", 7200)
dynamips_path = '/usr/bin/dynamips'
hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200)
hypervisor.start()
# give some time for Dynamips to start
time.sleep(0.01)
time.sleep(0.1)
output = hypervisor.read_stdout()
assert output

View File

@ -7,10 +7,9 @@ import os
@pytest.fixture(scope="module")
def hypervisor_manager(request):
cwd = os.path.dirname(os.path.abspath(__file__))
dynamips_path = os.path.join(cwd, "dynamips.stable")
dynamips_path = '/usr/bin/dynamips'
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
#manager.start_new_hypervisor()

View File

@ -9,7 +9,7 @@ import base64
@pytest.fixture
def router(request, hypervisor):
router = Router(hypervisor, "router", "c3725")
router = Router(hypervisor, "router", platform="c3725")
request.addfinalizer(router.delete)
return router
@ -127,9 +127,9 @@ def test_idlepc(router):
def test_idlemax(router):
assert router.idlemax == 1500 # default value
router.idlemax = 500
assert router.idlemax == 500
assert router.idlemax == 500 # default value
router.idlemax = 1500
assert router.idlemax == 1500
def test_idlesleep(router):
@ -172,7 +172,7 @@ def test_confreg(router):
def test_console(router):
assert router.console == router.hypervisor.baseconsole + router.id
assert router.console == 2001
new_console_port = router.console + 100
router.console = new_console_port
assert router.console == new_console_port
@ -180,7 +180,7 @@ def test_console(router):
def test_aux(router):
assert router.aux == router.hypervisor.baseaux + router.id
assert router.aux == 2501
new_aux_port = router.aux + 100
router.aux = new_aux_port
assert router.aux == new_aux_port

View File

@ -3,17 +3,28 @@ import os
import pytest
def no_iou():
cwd = os.path.dirname(os.path.abspath(__file__))
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
if os.path.isfile(iou_path):
return False
else:
return True
@pytest.fixture(scope="session")
def iou(request):
cwd = os.path.dirname(os.path.abspath(__file__))
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
iou_device = IOUDevice(iou_path, "/tmp")
iou_device = IOUDevice("IOU1", iou_path, "/tmp")
iou_device.start()
request.addfinalizer(iou_device.delete)
return iou_device
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
def test_iou_is_started(iou):
print(iou.command())
@ -21,6 +32,7 @@ def test_iou_is_started(iou):
assert iou.is_running()
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
def test_iou_restart(iou):
iou.stop()

View File

@ -6,9 +6,13 @@ import pytest
@pytest.fixture(scope="session")
def vpcs(request):
cwd = os.path.dirname(os.path.abspath(__file__))
vpcs_path = os.path.join(cwd, "vpcs")
vpcs_device = VPCSDevice(vpcs_path, "/tmp")
if os.path.isfile("/usr/bin/vpcs"):
vpcs_path = "/usr/bin/vpcs"
else:
cwd = os.path.dirname(os.path.abspath(__file__))
vpcs_path = os.path.join(cwd, "vpcs")
vpcs_device = VPCSDevice("VPCS1", vpcs_path, "/tmp")
vpcs_device.port_add_nio_binding(0, 'nio_tap:tap0')
vpcs_device.start()
request.addfinalizer(vpcs_device.delete)
return vpcs_device

View File

@ -2,6 +2,6 @@
envlist = py33, py34
[testenv]
commands = py.test [] -s tests
commands = python setup.py test
deps = -rdev-requirements.txt