Compare commits

...
134 Commits
Author SHA1 Message Date
grossmj f0c344939b Polish and bump up to version 1.0! 2014-10-20 18:40:05 -06:00
grossmj e261263aab Add path lookup directory for Qemu on OSX. 2014-10-20 11:56:30 -06:00
grossmj b88abb7c91 Remote servers and load-balancing (still things to improve). 2014-10-19 17:29:04 -06:00
grossmj 5f4b3c547b Bump to version 1.0.dev1. 2014-10-14 17:47:52 -06:00
grossmj f854752c84 Bump to version 1.0-beta4. 2014-10-14 17:20:28 -06:00
grossmj 4195bdc7dd Auto idle-pc feature and improvements/bug fixes for GNS3 preferences. 2014-10-13 19:53:17 -06:00
grossmj b68c11e33e Bump version to 1.0-beta4.dev2 2014-10-09 21:26:07 -06:00
Jeremy Grossmann 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
Jeremy Grossmann 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
Jeremy Grossmann 83cef60c0f Merge pull request #35 from planctechnologies/deadman2
Deadman switch support 2/2
2014-09-30 11:20:39 -06:00
Jeremy Grossmann e39c93c91a Merge pull request #34 from planctechnologies/deadman
Deadman switch support 1/2
2014-09-30 11:20:01 -06:00
grossmj 1a96a150bc Fix shemas for QEMU. 2014-09-30 11:15:15 -06:00
Jerry Seutter c66fbbdb36 Merge improved ssl and auth support 2014-09-29 18:24:28 -06:00
Jerry Seutter 03fb75437b Add cert and auth support to gns3server. 2014-09-29 18:19:35 -06:00
Jerry Seutter 3833803244 Copy deadman switch code from old repository into gns3server. 2014-09-29 18:14:07 -06:00
Jerry Seutter 7c446796fe gns3server now controls the deadman switch. 2014-09-29 18:09:16 -06:00
Jerry Seutter ee88d6f808 Merge branch 'master' into dev 2014-09-29 17:24:52 -06:00
grossmj 46495b9265 Merge remote-tracking branch 'origin/master' 2014-09-28 18:23:51 -06:00
grossmj a8193fa063 Split the PATH environment variable using os.pathsep 2014-09-28 18:23:27 -06:00
Jeremy Grossmann 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
Jeremy Grossmann 35f3434b2f Merge pull request #32 from noplay/osx_installation
Instruction for development on MacOS X
2014-09-27 13:56:28 -06:00
Daniel Lintott 20dc779fd8 Fix test for dynamips c7200 NPE (Default is now NPE-400) 2014-09-27 19:27:26 +01:00
Julien Duponchelle 04f670cb50 Instruction for development on MacOS X 2014-09-27 19:59:58 +02:00
grossmj 6dce005594 Bump to version 1.0-beta1.dev1. 2014-09-24 11:14:28 -06:00
grossmj a49f107af2 Bump to version 1.0-beta3. 2014-09-24 11:01:33 -06:00
grossmj e7141685cc Tweaks to support Qemu on Windows. 2014-09-23 21:38:51 -06:00
grossmj aca9e0de56 Qemu integration stage 2, support for ASA and IDS. 2014-09-22 21:24:55 -06:00
Jerry Seutter 3b465890b6 Increase sleep to work around Rackspace slowness 2014-09-22 09:10:30 -06:00
Jerry Seutter cf59240bef Bugfixes with cloud server communication 2014-09-21 21:41:51 -06:00
grossmj d1715baae1 Base QEMU support. 2014-09-18 15:47:43 -06:00
Jerry Seutter b132c901c9 Disabling auth from version string 2014-09-18 20:39:12 +00:00
Michael a0e2fe551a Added web user and password to start_server output 2014-09-15 21:25:09 -06:00
jseutter 800d4d91f9 Merge pull request #5 from planctechnologies/gns3-87-bugfixes
Gns-87 bugfixes
2014-09-11 14:48:26 -06:00
Michael 6c6c9200e4 Add CN support to cert as command line arg 2014-09-08 22:07:33 -06:00
Michael 4fa87005bc Enabled HTTP Auth, SSL and DMS disabling based on cloud.conf availability 2014-09-08 21:51:56 -06:00
Michael 17e4b51d18 Testing out dummy config 2014-09-08 20:45:36 -06:00
Jerry Seutter 6421367259 Importing changeset from gns3dms repo 2014-09-08 15:35:22 +00:00
jseutter 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
Michael f876a862c4 GNS3 server will now create the heardbeat file durining initialization 2014-09-06 21:13:09 -06:00
Michael ef492d4690 Update gns3dms to support cloud.conf 2014-09-06 20:46:06 -06:00
Michael 36e539382c Added support for cloud.conf file and startup script 2014-09-06 00:51:43 -06:00
Michael 6f9e0f6d2e Moved certs to .config 2014-09-03 22:19:59 -06:00
Michael b84dda3c8e HTTP auth added to file_upload and jsonrpc 2014-09-03 22:12:34 -06:00
Jerry Seutter e2f3d2aca8 Pull from dev 2014-09-03 20:39:02 +00:00
Michael 382e693fc8 Added authentication handler for basic auth check 2014-09-03 00:05:06 -06:00
Michael a95cc678e9 Added server.py ssl mode dependant on cert existence 2014-09-02 22:33:45 -06:00
Michael bcf0aae531 Added HOME support and cert dir to create_cert script 2014-09-02 22:17:06 -06:00
grossmj b483f87c2f Bump version to 1.0-beta3.dev1. 2014-09-02 15:49:39 -06:00
grossmj 9d2e18328b Bump version to 1.0-beta2. 2014-09-02 13:06:26 -06:00
Michael 174013da80 Merge pull request #2 from planctechnologies/deadman
GNS-42 - Move deadman switch into gns3server codebase
2014-08-31 23:50:41 -06:00
Michael 99a8f5f21a Added create_cert.sh and ssl_options to enable SSL 2014-08-30 01:32:48 -06:00
Jerry Seutter 5e72fcbe14 GNS-42 - Move deadman switch into gns3server codebase 2014-08-29 18:05:56 +00:00
Michael e688d96c36 Add start, stop, restart and heartbeat handling to DeadMan module 2014-08-28 23:06:28 -06:00
Michael 3845cab84b Adding initial module 2014-08-28 22:09:38 -06:00
Michael 98e3a2e088 Updated requirements.txt 2014-08-28 22:01:23 -06:00
grossmj 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
grossmj 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
grossmj 934404cc90 Change default port ranges. 2014-08-25 15:40:04 -06:00
grossmj 6e39630b9b Required VirtualBox wrapper is >= 9.1 2014-08-22 17:39:57 -06:00
grossmj 569a68a486 VirtualBox support refactoring. 2014-08-22 17:36:12 -06:00
grossmj 77c583ca39 Check if the VirtualBox COM service is installed on Windows. 2014-08-21 18:13:41 -06:00
grossmj ea05744e1c Force to rebuild the COM cache on Windows (for VirtualBox support). 2014-08-17 15:15:07 -06:00
grossmj e0f0c98ffd Do not look for vboxwrapper on non Windows platforms. 2014-08-13 12:11:41 -06:00
grossmj a8d740ef21 Fix version from 1.0beta2-dev1 to 1.0beta2.dev1 2014-08-11 22:13:21 -06:00
Jeremy Grossmann 90c8c4312c Merge pull request #27 from dlintott/master
Override check_origin and fix test suite
2014-08-10 15:27:49 -06:00
Daniel Lintott e5642546f1 Remove commented line, not needed anymore 2014-08-09 12:26:24 +01:00
Daniel Lintott 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
Daniel Lintott a4bc96af28 revert not installing requirements outside of tox 2014-08-08 19:20:20 +01:00
Daniel Lintott 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
Daniel Lintott ad287d3434 Remove un-needed imports 2014-08-08 19:14:36 +01:00
Daniel Lintott 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
Daniel Lintott 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
Daniel Lintott 8fc4667d2c Modify the TRAVIS environment check 2014-08-08 14:49:10 +01:00
Daniel Lintott 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
Daniel Lintott 578bb5741d Override check_origin from tornado.websocket 2014-08-06 22:43:37 +01:00
grossmj 0cfb446146 Bump version to beta1. 2014-07-18 22:50:22 -06:00
grossmj ae0feb8c9c Fixes packet capture when spaces are in the capture file path. 2014-07-18 22:43:01 -06:00
grossmj 7ff218a5fc Fixes issue when adding multiple VirtualBox VMs.
Remove early release dialog.
2014-07-18 21:10:46 -06:00
grossmj cdc778c159 Fixes issue with VirtualBox integration on Windows. 2014-07-18 18:04:21 -06:00
grossmj 19d24975c2 Remove privileged access checks for Dynamips. 2014-07-17 20:48:11 -06:00
grossmj 65d70bacfa Telnet to pipe support for VirtualBox. 2014-07-17 20:02:18 -06:00
grossmj 9ac2716826 Adds VirtualBox API for Python3 (not official). 2014-07-17 18:28:55 -06:00
grossmj 53ee3dc5cc Remove vboxapi_py3 package. 2014-07-17 18:24:21 -06:00
grossmj 64f78bb847 Fixes some inconsistencies when exporting configs. 2014-07-17 18:22:27 -06:00
grossmj f5cf7363da Work around for the c7200 reload bug in Dynamips <= 0.2.13. 2014-07-17 18:03:21 -06:00
grossmj 1fb4ab7e33 Minimal VirtualBox integration. 2014-07-17 15:28:02 -06:00
grossmj 0ef727ce4b Base for VirtualBox support. 2014-07-12 13:18:25 -06:00
grossmj 88e03ae312 Bump to version 1.0b1.dev1. 2014-07-11 16:44:42 -06:00
grossmj d6efd9ced0 Bump version to 1.0a8. 2014-07-09 17:08:22 -06:00
grossmj 6ffba35742 Use Windows interface names instead of their GUID (more user friendly).
Ask for an alternative interface if one cannot be found.
2014-07-08 14:01:45 -06:00
grossmj 9fc7650f3f Find an alternative IOS or IOU image if one cannot be found when importing a topology. 2014-07-06 16:34:38 -06:00
grossmj 551b98880b Default preferences for remote servers. 2014-07-04 15:18:13 -06:00
grossmj 7c99ee9de8 New feature: import/export device configs. 2014-07-03 18:56:37 -06:00
grossmj 9ac5b86431 Fixes IOU and VPCS renaming issues. 2014-07-03 09:02:25 -06:00
grossmj 329891fc83 Check for VPCS version >= 0.5b1 2014-07-02 15:04:39 -06:00
grossmj 15bc2221b9 Bump to version 1.0a8.dev1 and reactivate cloud features. 2014-07-01 08:19:19 -06:00
grossmj 4b5c38495a Change default projects directory from ~/Documents/GNS3/projects to ~/GNS3/projects. 2014-06-29 10:14:15 -06:00
grossmj c9a729b369 Bump version to 1.0a7. 2014-06-29 03:59:35 -06:00
Jeremy Grossmann 47d43a91ff Merge pull request #21 from planctechnologies/master
Create cloud server images
2014-06-27 11:04:17 -06:00
grossmj 9bc0287540 New feature: packet capture for IOU (not working, issue with iouyap). 2014-06-27 09:42:34 -06:00
Jerry Seutter fd6e4954e4 Merge branch 'master' into dev 2014-06-27 14:26:58 +00:00
Jerry Seutter ab42b41d39 Merge remote-tracking branch 'origin/gns-80-cloud-image' into dev
Conflicts:
	cloud-image/create_image.py
2014-06-27 14:19:17 +00:00
grossmj 33787d486a New feature: packet capture for the Ethernet hub and Ethernet, ATM and Frame relay switches. 2014-06-27 07:26:47 -06:00
Jerry Seutter ff0201bb4f Syntax cleanup 2014-06-26 14:35:19 +00:00
Kieron Bulloch 868935f5e8 Improving readability. 2014-06-26 13:44:03 +00:00
Kieron Bulloch d6c5dffae6 Switching script creation method to use string.Template 2014-06-26 13:44:03 +00:00
Kieron Bulloch e16c269f6d Adding readme.txt 2014-06-26 13:44:03 +00:00
Kieron Bulloch 7d8aee98fd Adding GNS3-server code source info to default image name.
Fixing spacing, etc before pull request.
2014-06-26 13:44:03 +00:00
Kieron Bulloch b4f4bad21a Moving cloud image creation script to gns3-server project. 2014-06-26 13:44:03 +00:00
grossmj 606f773f3d New feature: packet capture for IOS routers. 2014-06-26 03:06:58 -06:00
Kieron Bulloch 2d4656621b Improving readability. 2014-06-24 23:34:50 -04:00
Kieron Bulloch aedd750251 Switching script creation method to use string.Template 2014-06-24 23:07:28 -04:00
grossmj 14bb12d3fb Check for sticky bit when checking for executable access. 2014-06-21 06:53:47 -06:00
grossmj 49506ada3f Fixes inconsistencies with startup and private config paths when renaming an IOS router. 2014-06-18 07:22:57 -06:00
grossmj 587ddf7646 IOU: rename startup-config to initial-config because it makes more sense. 2014-06-18 06:08:00 -06:00
Kieron Bulloch 8dc3f288b3 Adding readme.txt 2014-06-17 00:11:25 -04:00
Kieron Bulloch 8b3c5c54a4 Adding GNS3-server code source info to default image name.
Fixing spacing, etc before pull request.
2014-06-17 00:02:17 -04:00
Kieron Bulloch a01cdd9978 Moving cloud image creation script to gns3-server project. 2014-06-16 01:20:35 -04:00
grossmj cb763e0926 Use Dynamips, IOU and VPCS identifiers to correctly load a topology. 2014-06-15 05:18:33 -06:00
grossmj f9ee38dd55 Fixes issues to restore the correct working directories for IOU and VPCS devices when loading a project.
Prevent multiple clients to use the same server (this is not supported yet).
2014-06-10 09:33:27 -06:00
grossmj 9ef715e341 Update README. 2014-05-31 14:43:07 -06:00
grossmj 3a57539f77 Bump to version alpha7.dev1 2014-05-31 10:51:19 -06:00
grossmj e5f5228329 Bump to version 1.0-alpha6. 2014-05-30 13:49:52 -06:00
grossmj 61ef750da3 Replace decode errors when reading device configs. 2014-05-29 12:59:13 -06:00
grossmj e817c13738 Catch BlockingIOError in ioucon. 2014-05-29 03:10:45 -06:00
grossmj a0a5705fd8 Fixes issue with Frozen server and templates directory. 2014-05-29 01:30:18 -06:00
grossmj 7b58f14681 Some PEP8 style fixes. 2014-05-28 06:26:20 -06:00
grossmj a39a693cda Hostname management refactoring. 2014-05-27 11:23:06 -06:00
grossmj 3a0439c9ae New hostnames management for the Dynamips module. 2014-05-27 03:59:22 -06:00
grossmj d7b9ed33f8 Bump to version 1.0a6.dev2 2014-05-25 01:51:57 -06:00
grossmj f2fbdf618f Fixes privileged access checks for IOU. 2014-05-22 13:06:29 -06:00
grossmj 9da5aa1107 Fixes validation issue with c2600 XM chassis. 2014-05-22 10:54:34 -06:00
grossmj 0f5d2927df Fixes VPCS start on Linux/UNIX. Fixes #15. 2014-05-22 10:14:09 -06:00
grossmj f1d346f958 Bump to version 1.0a6.dev1 2014-05-21 23:34:19 -06:00
115 changed files with 12891 additions and 1467 deletions

No files matched your search

+11 -6
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:
+17 -2
View File
@@ -1,7 +1,7 @@
GNS3-server
===========
New GNS3 server repository (alpha stage).
New GNS3 server repository (beta stage).
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.
@@ -34,4 +34,19 @@ Please use our all-in-one installer.
Mac OS X
--------
DMG package is not available yet.
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
+5
View File
@@ -0,0 +1,5 @@
export OS_USERNAME=username
export OS_PASSWORD=""
export OS_TENANT_NAME=000000
export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0/
export OS_REGION_NAME=ord
+243
View File
@@ -0,0 +1,243 @@
""" Create a new GNS3 Server Rackspace image with the provided options. """
import argparse
import getpass
import os
import sys
import uuid
from fabric.api import env
from fabric.contrib.files import exists
from github import Github
from novaclient.v1_1 import client
from string import Template
from time import sleep
POLL_SEC = 20
GNS3_REPO = 'gns3/gns3-server'
PLANC_REPO = 'planctechnologies/gns3-server'
OS_AUTH_URL = 'https://identity.api.rackspacecloud.com/v2.0/'
UBUNTU_BASE_ID = '5cc098a5-7286-4b96-b3a2-49f4c4f82537'
def main():
"""
Get the user options and perform the image creation.
Creates a new instance, installs the required software, creates an image
from the instance, and then deletes the instance.
"""
github = Github()
args = get_cli_args()
if args.username:
username = args.username
else:
if 'OS_USERNAME' in os.environ:
username = os.environ.get('OS_USERNAME')
else:
username = raw_input('Enter Rackspace username: ')
if args.password:
password = args.password
else:
if 'OS_PASSWORD' in os.environ:
password = os.environ.get('OS_PASSWORD')
else:
password = getpass.getpass('Enter Rackspace password: ')
if args.tenant:
tenant = args.tenant
else:
if 'OS_TENANT_NAME' in os.environ:
tenant = os.environ.get('OS_TENANT_NAME')
else:
tenant = raw_input('Enter Rackspace Tenant ID: ')
if args.region:
region = args.region
else:
if 'OS_REGION_NAME' in os.environ:
region = os.environ.get('OS_REGION_NAME')
else:
region = raw_input('Enter Rackspace Region Name: ')
if args.source == 'release':
# get the list of releases, present them to the user, save the url
repo = github.get_repo(GNS3_REPO)
keyword = "tag"
i = 1
branch_opts = {}
for tag in repo.get_tags():
branch_opts[i] = tag.name
i += 1
elif args.source == 'dev':
# get the list of dev branches, present them to the user, save the url
repo = github.get_repo(PLANC_REPO)
keyword = "branch"
i = 1
branch_opts = {}
for branch in repo.get_branches():
branch_opts[i] = branch.name
i += 1
prompt_text = "Select a %s" % keyword
selected_branch = prompt_user_select(branch_opts, prompt_text)
if args.image_name:
image_name = args.image_name
else:
image_name = "gns3-%s-%s-%s" % (args.source, selected_branch,
uuid.uuid4().hex[0:4])
if args.on_boot:
on_boot = True
else:
on_boot = False
startup_script = create_script(repo.svn_url, selected_branch, on_boot)
server_name = uuid.uuid4().hex
instance = create_instance(username, password, tenant, region, server_name,
startup_script)
passwd = uuid.uuid4().hex
instance.change_password(passwd)
# 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"
env.password = passwd
sys.stdout.write("Installing software...")
sys.stdout.flush()
while True:
if exists('/tmp/gns-install-complete'):
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
image_id = create_image(username, password, tenant, region, instance,
image_name)
instance.delete()
def prompt_user_select(opts, text="Please select"):
""" Ask the user to select an option from the provided list. """
print("%s" % text)
print("=" * len(text))
for o in opts:
print("(%s)\t%s" % (o, opts[o]))
while True:
selected = raw_input("Select: ")
try:
return opts[int(selected)]
except (KeyError, ValueError):
print("Invalid selection. Try again")
def create_instance(username, password, tenant, region, server_name, script,
auth_url=OS_AUTH_URL):
""" Create a new instance. """
sys.stdout.write("Creating instance...")
sys.stdout.flush()
nc = client.Client(username, password, tenant, auth_url,
region_name=region)
server = nc.servers.create(server_name, UBUNTU_BASE_ID, 2,
config_drive=True, userdata=script)
while True:
server = nc.servers.get(server.id)
if server.status == 'ACTIVE':
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
return server
def create_script(git_url, git_branch, on_boot):
""" Create the start-up script. """
script_template = Template(open('script_template', 'r').read())
params = {'git_url': git_url, 'git_branch': git_branch, 'rc_local': ''}
if on_boot:
params['rc_local'] = "echo '/usr/local/bin/gns3-server' >> /etc/rc.local"
return script_template.substitute(params)
def create_image(username, password, tenant, region, server,
image_name, auth_url=OS_AUTH_URL):
""" Create a Rackspace image based on the server instance. """
nc = client.Client(username, password, tenant, auth_url,
region_name=region)
sys.stdout.write("Creating image %s..." % image_name)
sys.stdout.flush()
image_id = server.create_image(image_name)
while True:
server = nc.servers.get(server.id)
if getattr(server, 'OS-EXT-STS:task_state') is None:
break
sleep(POLL_SEC)
sys.stdout.write(".")
sys.stdout.flush()
print("Done.")
return image_id
def get_cli_args():
""" Parse the CLI input. """
parser = argparse.ArgumentParser(
description='Create a new GNS3 image',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'--rackspace_username', dest='username', action='store')
parser.add_argument(
'--rackspace_password', dest='password', action='store')
parser.add_argument(
'--rackspace_tenant', dest='tenant', action='store')
parser.add_argument(
'--rackspace_region', dest='region', action='store')
parser.add_argument(
'--source', dest='source', action='store', choices=['release', 'dev'],
default='release', help='specify the gns3-server source location')
parser.add_argument(
'--branch', dest='branch', action='store',
help='specify the branch/tag')
parser.add_argument(
'--start-on-boot', dest='on_boot', action='store_true',
default=False, help='start the GNS3-server when the image boots')
parser.add_argument(
'--image-name', dest='image_name', action='store',
help='the name of the image to be created')
return parser.parse_args()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
fabric
pygithub
python-novaclient
+10
View File
@@ -0,0 +1,10 @@
create_image.py:
- uses fabric, which doesn't support Python 3
- prompts for Rackspace credentials if environment variables not set
- see .novarc for example env variables
- note that the novaclient library uses the Rackspace password and -not-
the API key
- use '--help' for help with arguments
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
apt-get -y install git
apt-get -y install python3-setuptools
apt-get -y install python3-netifaces
apt-get -y install python3-pip
mkdir -p /opt/gns3
pushd /opt/gns3
git clone --branch ${git_branch} ${git_url}
cd gns3-server
pip3 install -r dev-requirements.txt
python3 ./setup.py install
${rc_local}
touch /tmp/gns-install-complete
+26
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
Whitespace-only changes.
+179
View File
@@ -0,0 +1,179 @@
# -*- 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 libcloud.compute.base import NodeAuthSSHKey
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
from .exceptions import Unauthorized, ApiError
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
}
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 create_instance(self, name, size, image, keypair):
"""
Create a new instance with the supplied attributes.
Return a Node object.
"""
auth_key = NodeAuthSSHKey(keypair.public_key)
try:
return self.driver.create_node(
name=name,
size=size,
image=image,
auth=auth_key
)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
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. """
return self.driver.list_nodes()
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 list_key_pairs(self):
""" Return a list of Key Pairs. """
return self.driver.list_key_pairs()
+45
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
+225
View File
@@ -0,0 +1,225 @@
# -*- 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 .exceptions import ItemNotFound, ApiError
from ..version import __version__
import logging
log = logging.getLogger(__name__)
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
ENDPOINT_ARGS_MAP]
GNS3IAS_URL = 'http://localhost:8888' # TODO find a place for this value
class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key):
super(RackspaceCtrl, self).__init__(username, api_key)
# 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.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
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)
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 _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 _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)
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 = GNS3IAS_URL+"/images/grant_access"
params = {
"user_id": username,
"user_region": region,
"gns3_version": gns3_version,
}
response = requests.get(endpoint, params=params)
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.username and self.region):
return []
try:
response = self._get_shared_images(self.username, self.region, __version__)
shared_images = json.loads(response)
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)
+401
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
--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["region"] is None:
print("You need to specify a 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)
+24
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
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()."""
+70
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
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)
+7 -7
View File
@@ -27,7 +27,7 @@ import logging
log = logging.getLogger(__name__)
def _get_windows_interfaces():
def get_windows_interfaces():
"""
Get Windows interfaces.
@@ -42,9 +42,9 @@ def _get_windows_interfaces():
for adapter in service.InstancesOf("Win32_NetworkAdapter"):
if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7:
# adapter is connected or media disconnected
name = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
interfaces.append({"name": name,
"description": adapter.NetConnectionID})
npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
interfaces.append({"id": npf_interface,
"name": adapter.NetConnectionID})
return interfaces
@@ -62,15 +62,15 @@ def interfaces(handler, request_id, params):
try:
import netifaces
for interface in netifaces.interfaces():
response.append({"name": interface,
"description": interface})
response.append({"id": interface,
"name": interface})
except ImportError:
message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3"
handler.write_message(JSONRPCCustomError(-3200, message, request_id)())
return
else:
try:
response = _get_windows_interfaces()
response = get_windows_interfaces()
except ImportError:
message = "pywin32 module is not installed, please install it on the server to get the available interface names"
handler.write_message(JSONRPCCustomError(-3200, message, request_id)())
+1
View File
@@ -23,6 +23,7 @@ Sends version to requesting clients in JSON-RPC Websocket handler.
from ..version import __version__
from ..jsonrpc import JSONRPCResponse
def server_version(handler, request_id, params):
"""
Builtin destination to return the server version.
+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
+6 -1
View File
@@ -62,16 +62,21 @@ class Config(object):
# 5: server.conf in the current working directory
home = os.path.expanduser("~")
self._cloud_config = 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_config]
self._config = configparser.ConfigParser()
self.read_config()
def list_cloud_config_file(self):
return self._cloud_config
def read_config(self):
"""
Read the configuration files.
+88
View File
@@ -0,0 +1,88 @@
# -*- 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 %s: %s" %(auth_status, user))
try:
redirect_to = self.get_secure_cookie("login_success_redirect_to")
except tornado.web.MissingArgumentError:
redirect_to = "/"
self.redirect(redirect_to)
+8 -8
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.
@@ -38,15 +39,14 @@ class FileUploadHandler(tornado.web.RequestHandler):
:param request: Tornado Request instance
"""
def __init__(self, application, request):
def __init__(self, application, request, **kwargs):
# get the upload directory from the configuration file
super().__init__(application, request, **kwargs)
config = Config.instance()
server_config = config.get_default_section()
# default projects directory is "~/Documents/GNS3/images"
self._upload_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images")))
self._upload_dir = os.path.expandvars(
os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images")))
self._host = request.host
try:
os.makedirs(self._upload_dir)
log.info("upload directory '{}' created".format(self._upload_dir))
@@ -55,8 +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.websocket.WebSocketHandler.__init__(self, application, request)
@tornado.web.authenticated
def get(self):
"""
Invoked on GET request.
@@ -73,6 +72,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
path=path,
items=items)
@tornado.web.authenticated
def post(self):
"""
Invoked on POST request.
+24 -4
View File
@@ -22,17 +22,19 @@ 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
from ..jsonrpc import JSONRPCMethodNotFound
from ..jsonrpc import JSONRPCNotification
from ..jsonrpc import JSONRPCCustomError
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.
@@ -51,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):
"""
@@ -106,8 +111,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
if destination.startswith("builtin"):
log.debug("registering {} as a built-in destination".format(destination))
else:
log.debug("registering {} as a destination for the {} module".format(destination,
module))
log.debug("registering {} as a destination for the {} module".format(destination, module))
cls.destinations[destination] = module
def open(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):
"""
@@ -143,8 +155,16 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
if jsonrpc_version != self.version:
return self.write_message(JSONRPCInvalidRequest()())
if len(self.clients) > 1:
#TODO: multiple client support
log.warn("GNS3 server doesn't support multiple clients yet")
return self.write_message(JSONRPCCustomError(-3200,
"There are {} clients connected, the GNS3 server cannot handle multiple clients yet".format(len(self.clients)),
request_id)())
if method not in self.destinations:
if request_id:
log.warn("JSON-RPC method not found: {}".format(method))
return self.write_message(JSONRPCMethodNotFound(request_id)())
else:
# This is a notification, silently ignore this error...
+2 -2
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__}
+1 -1
View File
@@ -161,7 +161,7 @@ class JSONRPCRequest(JSONRPCObject):
def __init__(self, method, params=None, request_id=None):
JSONRPCObject.__init__(self)
if request_id == None:
if request_id is None:
request_id = str(uuid.uuid4())
self.id = request_id
self.method = method
+1
View File
@@ -70,6 +70,7 @@ def locale_check():
locale.setlocale(locale.LC_ALL, (language, "UTF-8"))
except locale.Error as e:
log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e))
raise SystemExit
else:
log.info("current locale is {}.{}".format(language, encoding))
-1
View File
@@ -15,7 +15,6 @@
# 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 imp
import inspect
import pkgutil
from .modules import IModule
+4 -2
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]
MODULES.append(VPCS)
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
+12 -5
View File
@@ -23,6 +23,7 @@ import sys
import os
import struct
import socket
import stat
import errno
import time
@@ -96,7 +97,7 @@ def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
connection_success = False
begin = time.time()
last_exception = None
while (time.time() - begin < wait):
while time.time() - begin < wait:
time.sleep(0.01)
try:
with socket.create_connection((host, port), socket_timeout):
@@ -107,22 +108,28 @@ def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
connection_success = True
break
return (connection_success, last_exception)
return connection_success, last_exception
def has_privileged_access(executable, device):
def has_privileged_access(executable):
"""
Check if an executable can access Ethernet and TAP devices in
RAW mode.
:param executable: executable path
:param device: device name
:returns: True or False
"""
# we are root, so we should have privileged access too
if sys.platform.startswith("win"):
# do not check anything on Windows
return True
if os.geteuid() == 0:
# we are root, so we should have privileged access.
return True
if os.stat(executable).st_mode & stat.S_ISVTX == stat.S_ISVTX:
# the executable has a sticky bit.
return True
# test if the executable has the CAP_NET_RAW capability (Linux only)
+11 -1
View File
@@ -19,6 +19,7 @@
Base class (interface) for modules
"""
import os
import sys
import traceback
import gns3server.jsonrpc as jsonrpc
@@ -26,6 +27,7 @@ import multiprocessing
import zmq
import signal
from gns3server.config import Config
from jsonschema import validate, ValidationError
import logging
@@ -45,6 +47,9 @@ class IModule(multiprocessing.Process):
def __init__(self, name, *args, **kwargs):
config = Config.instance()
server_config = config.get_default_section()
self._images_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images")))
multiprocessing.Process.__init__(self, name=name)
self._context = None
self._ioloop = None
@@ -288,7 +293,7 @@ class IModule(multiprocessing.Process):
"""
# check if we have a request
if request == None:
if request is None:
self.send_param_error()
return False
log.debug("received request {}".format(request))
@@ -328,3 +333,8 @@ class IModule(multiprocessing.Process):
cls.modules[module][destination] = method
return method
return wrapper
@property
def images_directory(self):
return self._images_dir
+164
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()
+50 -13
View File
@@ -27,11 +27,13 @@ import shutil
import glob
import socket
from gns3server.modules import IModule
import gns3server.jsonrpc as jsonrpc
from gns3server.config import Config
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
@@ -104,8 +106,27 @@ class Dynamips(IModule):
def __init__(self, name, *args, **kwargs):
IModule.__init__(self, name, *args, **kwargs)
# get the Dynamips location
config = Config.instance()
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(os.pathsep)
# look for Dynamips in the current working directory and $PATH
for path in paths:
try:
if "dynamips" in os.listdir(path) and os.access(os.path.join(path, "dynamips"), os.X_OK):
self._dynamips = os.path.join(path, "dynamips")
break
except OSError:
continue
if not self._dynamips:
log.warning("dynamips binary couldn't be found!")
elif not os.access(self._dynamips, os.X_OK):
log.warning("dynamips is not executable")
IModule.__init__(self, name, *args, **kwargs)
self._hypervisor_manager = None
self._hypervisor_manager_settings = {}
self._routers = {}
@@ -116,8 +137,7 @@ class Dynamips(IModule):
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._dynamips = ""
self._host = kwargs["host"]
self._host = dynamips_config.get("host", kwargs["host"])
if not sys.platform.startswith("win32"):
#FIXME: pickle issues Windows
@@ -249,8 +269,8 @@ class Dynamips(IModule):
if not os.access(self._dynamips, os.X_OK):
raise DynamipsError("Dynamips {} is not executable".format(self._dynamips))
workdir = os.path.join(self._working_dir, "dynamips")
try:
workdir = os.path.join(self._working_dir, "dynamips")
os.makedirs(workdir)
except FileExistsError:
pass
@@ -278,11 +298,12 @@ class Dynamips(IModule):
Optional request parameters:
- working_dir (path to a working directory)
- project_name
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
return
@@ -342,7 +363,7 @@ class Dynamips(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
@@ -379,8 +400,21 @@ class Dynamips(IModule):
nio.connect(rhost, rport)
elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"]
if sys.platform.startswith("win"):
# replace the interface name by the GUID on Windows
interfaces = get_windows_interfaces()
npf_interface = None
for interface in interfaces:
if interface["name"] == ethernet_device:
npf_interface = interface["id"]
if not npf_interface:
raise DynamipsError("Could not find interface {} on this host".format(ethernet_device))
else:
ethernet_device = npf_interface
nio = NIO_GenericEthernet(node.hypervisor, ethernet_device)
elif request["nio"]["type"] == "nio_linux_ethernet":
if sys.platform.startswith("win"):
raise DynamipsError("This NIO type is not supported on Windows")
ethernet_device = request["nio"]["ethernet_device"]
nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device)
elif request["nio"]["type"] == "nio_tap":
@@ -415,7 +449,6 @@ class Dynamips(IModule):
port,
host))
response = {"lport": port}
return response
def set_ghost_ios(self, router):
@@ -445,9 +478,13 @@ class Dynamips(IModule):
ghost.ghost_status = 1
ghost.ghost_file = ghost_instance
ghost.ram = router.ram
ghost.start()
ghost.stop()
ghost.clean_delete()
try:
ghost.start()
ghost.stop()
except DynamipsError:
raise
finally:
ghost.clean_delete()
if router.ghost_file != ghost_instance:
# set the ghost file to the router
@@ -476,7 +513,7 @@ class Dynamips(IModule):
raise DynamipsError("Could not create configs directory: {}".format(e))
try:
with open(local_base_config, "r") as f:
with open(local_base_config, "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "")
@@ -498,7 +535,7 @@ class Dynamips(IModule):
"""
log.info("creating config file {} from base64".format(destination_config_path))
config = base64.decodestring(config_base64.encode("utf-8")).decode("utf-8")
config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', router.name)
config_dir = os.path.dirname(destination_config_path)
@@ -63,7 +63,7 @@ class Adapter(object):
False otherwise.
"""
if self._wics[wic_slot_id] == None:
if self._wics[wic_slot_id] is None:
return True
return False
+86 -6
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import os
from gns3server.modules import IModule
from ..nodes.atm_switch import ATMSwitch
from ..dynamips_error import DynamipsError
@@ -26,6 +27,8 @@ from ..schemas.atmsw import ATMSW_UPDATE_SCHEMA
from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_START_CAPTURE_SCHEMA
from ..schemas.atmsw import ATMSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -38,7 +41,7 @@ class ATMSW(object):
"""
Creates a new ATM switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -49,13 +52,10 @@ class ATMSW(object):
"""
# validate the request
if request and not self.validate_request(request, ATMSW_CREATE_SCHEMA):
if not self.validate_request(request, ATMSW_CREATE_SCHEMA):
return
name = None
if request and "name" in request:
name = request["name"]
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -313,3 +313,83 @@ class ATMSW(object):
return
self.send_response(True)
@IModule.route("dynamips.atmsw.start_capture")
def atmsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_START_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(atmsw.hypervisor.working_dir, "captures", capture_file_name)
atmsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.atmsw.stop_capture")
def atmsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_STOP_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
try:
atmsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+87 -7
View File
@@ -15,6 +15,7 @@
# 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 os
from gns3server.modules import IModule
from ..nodes.hub import Hub
from ..dynamips_error import DynamipsError
@@ -25,6 +26,8 @@ from ..schemas.ethhub import ETHHUB_UPDATE_SCHEMA
from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_START_CAPTURE_SCHEMA
from ..schemas.ethhub import ETHHUB_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -37,7 +40,7 @@ class ETHHUB(object):
"""
Creates a new Ethernet hub.
Optional request parameters:
Mandatory request parameters:
- name (hub name)
Response parameters:
@@ -48,13 +51,10 @@ class ETHHUB(object):
"""
# validate the request
if request and not self.validate_request(request, ETHHUB_CREATE_SCHEMA):
if not self.validate_request(request, ETHHUB_CREATE_SCHEMA):
return
name = None
if request and "name" in request:
name = request["name"]
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -239,7 +239,7 @@ class ETHHUB(object):
self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.ethhub.delete_nio")
def ethsw_delete_nio(self, request):
def ethhub_delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
@@ -271,3 +271,83 @@ class ETHHUB(object):
return
self.send_response(True)
@IModule.route("dynamips.ethhub.start_capture")
def ethhub_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_START_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethhub.hypervisor.working_dir, "captures", capture_file_name)
ethhub.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethhub.stop_capture")
def ethhub_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
try:
ethhub.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+86 -6
View File
@@ -15,6 +15,7 @@
# 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 os
from gns3server.modules import IModule
from ..nodes.ethernet_switch import EthernetSwitch
from ..dynamips_error import DynamipsError
@@ -25,6 +26,8 @@ from ..schemas.ethsw import ETHSW_UPDATE_SCHEMA
from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_START_CAPTURE_SCHEMA
from ..schemas.ethsw import ETHSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -37,7 +40,7 @@ class ETHSW(object):
"""
Creates a new Ethernet switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -48,13 +51,10 @@ class ETHSW(object):
"""
# validate the request
if request and not self.validate_request(request, ETHSW_CREATE_SCHEMA):
if not self.validate_request(request, ETHSW_CREATE_SCHEMA):
return
name = None
if request and "name" in request:
name = request["name"]
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -300,3 +300,83 @@ class ETHSW(object):
return
self.send_response(True)
@IModule.route("dynamips.ethsw.start_capture")
def ethsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_START_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethsw.hypervisor.working_dir, "captures", capture_file_name)
ethsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethsw.stop_capture")
def ethsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
try:
ethsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+86 -6
View File
@@ -15,6 +15,7 @@
# 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 os
from gns3server.modules import IModule
from ..nodes.frame_relay_switch import FrameRelaySwitch
from ..dynamips_error import DynamipsError
@@ -25,6 +26,8 @@ from ..schemas.frsw import FRSW_UPDATE_SCHEMA
from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA
from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA
from ..schemas.frsw import FRSW_START_CAPTURE_SCHEMA
from ..schemas.frsw import FRSW_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -37,7 +40,7 @@ class FRSW(object):
"""
Creates a new Frame-Relay switch.
Optional request parameters:
Mandatory request parameters:
- name (switch name)
Response parameters:
@@ -48,13 +51,10 @@ class FRSW(object):
"""
# validate the request
if request and not self.validate_request(request, FRSW_CREATE_SCHEMA):
if not self.validate_request(request, FRSW_CREATE_SCHEMA):
return
name = None
if request and "name" in request:
name = request["name"]
name = request["name"]
try:
if not self._hypervisor_manager:
self.start_hypervisor_manager()
@@ -292,3 +292,83 @@ class FRSW(object):
return
self.send_response(True)
@IModule.route("dynamips.frsw.start_capture")
def frsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_START_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(frsw.hypervisor.working_dir, "captures", capture_file_name)
frsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.frsw.stop_capture")
def frsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_STOP_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
try:
frsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
+214 -14
View File
@@ -17,6 +17,7 @@
import os
import base64
import time
from gns3server.modules import IModule
from ..dynamips_error import DynamipsError
@@ -56,8 +57,12 @@ from ..schemas.vm import VM_STOP_SCHEMA
from ..schemas.vm import VM_SUSPEND_SCHEMA
from ..schemas.vm import VM_RELOAD_SCHEMA
from ..schemas.vm import VM_UPDATE_SCHEMA
from ..schemas.vm import VM_START_CAPTURE_SCHEMA
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
@@ -105,12 +110,12 @@ class VM(object):
Creates a new VM (router).
Mandatory request parameters:
- name (vm name)
- platform (platform name e.g. c7200)
- image (path to IOS image)
- ram (amount of RAM in MB)
Optional request parameters:
- name (vm name)
- console (console port number)
- aux (auxiliary console port number)
- mac_addr (MAC address)
@@ -127,16 +132,17 @@ class VM(object):
if not self.validate_request(request, VM_CREATE_SCHEMA):
return
name = None
if "name" in request:
name = request["name"]
name = request["name"]
platform = request["platform"]
image = request["image"]
ram = request["ram"]
hypervisor = None
chassis = None
if "chassis" in request:
chassis = request["chassis"]
chassis = request.get("chassis")
router_id = request.get("router_id")
updated_image_path = os.path.join(self.images_directory, image)
if os.path.isfile(updated_image_path):
image = updated_image_path
try:
@@ -149,12 +155,13 @@ class VM(object):
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram)
if chassis:
router = PLATFORMS[platform](hypervisor, name, chassis=chassis)
router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis)
else:
router = PLATFORMS[platform](hypervisor, name)
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"]
@@ -390,8 +397,8 @@ class VM(object):
response = {}
try:
startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}.cfg".format(router.name))
private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}-private.cfg".format(router.name))
startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id))
private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id))
# a new startup-config has been pushed
if "startup_config_base64" in request:
@@ -410,7 +417,7 @@ class VM(object):
router.set_config(request["startup_config"])
else:
router.set_config(request["startup_config"])
response["startup_config"] = request["startup_config"]
response["startup_config"] = request["startup_config"]
if "private_config" in request:
if os.path.isfile(request["private_config"]) and request["private_config"] != private_config_path:
@@ -419,7 +426,7 @@ class VM(object):
router.set_config(router.startup_config, request["private_config"])
else:
router.set_config(router.startup_config, request["private_config"])
response["private_config"] = request["private_config"]
response["private_config"] = request["private_config"]
except DynamipsError as e:
self.send_custom_error(str(e))
@@ -487,6 +494,90 @@ class VM(object):
self.send_response(response)
@IModule.route("dynamips.vm.start_capture")
def vm_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_START_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(router.hypervisor.working_dir, "captures", capture_file_name)
router.start_capture(slot, port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.vm.stop_capture")
def vm_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_STOP_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
try:
router.stop_capture(slot, port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("dynamips.vm.save_config")
def vm_save_config(self, request):
"""
@@ -534,6 +625,45 @@ class VM(object):
except DynamipsError as e:
log.warn("could not save config to {}: {}".format(router.startup_config, e))
@IModule.route("dynamips.vm.export_config")
def vm_export_config(self, request):
"""
Export the config from a router
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- startup_config_base64 (startup-config base64 encoded)
- private_config_base64 (private-config base64 encoded)
- False if no configuration can be extracted
"""
# validate the request
if not self.validate_request(request, VM_EXPORT_CONFIG_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
response = {}
try:
startup_config_base64, private_config_base64 = router.extract_config()
if startup_config_base64:
response["startup_config_base64"] = startup_config_base64
if private_config_base64:
response["private_config_base64"] = private_config_base64
except DynamipsError:
self.send_custom_error("unable to extract configs from the NVRAM")
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("dynamips.vm.idlepcs")
def vm_idlepcs(self, request):
"""
@@ -576,6 +706,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):
"""
@@ -61,7 +61,7 @@ class DynamipsHypervisor(object):
self._udp_end_port_range = 20000
self._nio_udp_auto_instances = {}
self._version = "N/A"
self._timeout = 30
self._timeout = timeout
self._socket = None
self._uuid = None
@@ -80,9 +80,7 @@ class DynamipsHypervisor(object):
host = self._host
try:
self._socket = socket.create_connection((host,
self._port),
self._timeout)
self._socket = socket.create_connection((host, self._port), self._timeout)
except OSError as e:
raise DynamipsError("Could not connect to server: {}".format(e))
@@ -477,7 +475,7 @@ class DynamipsHypervisor(object):
self.socket.sendall(command.encode('utf-8'))
except OSError as e:
raise DynamipsError("Lost communication with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
.format(host=self._host, port=self._port, error=e))
# Now retrieve the result
data = []
@@ -488,7 +486,7 @@ class DynamipsHypervisor(object):
buf += chunk.decode("utf-8")
except OSError as e:
raise DynamipsError("Communication timed out with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e))
.format(host=self._host, port=self._port, error=e))
# If the buffer doesn't end in '\n' then we can't be done
try:
+7 -7
View File
@@ -70,7 +70,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: id (integer)
"""
return(self._id)
return self._id
@property
def started(self):
@@ -90,7 +90,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: path to Dynamips
"""
return(self._path)
return self._path
@path.setter
def path(self, path):
@@ -110,7 +110,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: port number (integer)
"""
return(self._port)
return self._port
@port.setter
def port(self, port):
@@ -130,7 +130,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: host/address (string)
"""
return(self._host)
return self._host
@host.setter
def host(self, host):
@@ -232,7 +232,7 @@ class Hypervisor(DynamipsHypervisor):
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() == None:
if self._process.poll() is None:
log.warn("Dynamips process {} is still running".format(self._process.pid))
if self._stdout_file and os.access(self._stdout_file, os.W_OK):
@@ -251,7 +251,7 @@ class Hypervisor(DynamipsHypervisor):
output = ""
if self._stdout_file and os.access(self._stdout_file, os.R_OK):
try:
with open(self._stdout_file) as file:
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))
@@ -264,7 +264,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: True or False
"""
if self._process and self._process.poll() == None:
if self._process and self._process.poll() is None:
return True
return False
@@ -19,6 +19,7 @@
Manages Dynamips hypervisors (load-balancing etc.)
"""
from gns3server.config import Config
from .hypervisor import Hypervisor
from .dynamips_error import DynamipsError
from ..attic import find_unused_port
@@ -39,10 +40,6 @@ 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 base_port: base TCP port for hypervisors
:param base_console: base TCP port for consoles
:param base_aux: base TCP port for auxiliary consoles
:param base_udp: base UDP port for UDP tunnels
"""
def __init__(self, path, working_dir, host='127.0.0.1'):
@@ -51,21 +48,24 @@ class HypervisorManager(object):
self._path = path
self._working_dir = working_dir
self._host = host
self._hypervisor_start_port_range = 7200
self._hypervisor_end_port_range = 7700
self._console_start_port_range = 2001
self._console_end_port_range = 2500
self._aux_start_port_range = 2501
self._aux_end_port_range = 3000
self._udp_start_port_range = 10001
self._udp_end_port_range = 20000
self._ghost_ios_support = True
self._mmap_support = True
self._jit_sharing_support = False
self._sparse_memory_support = True
self._allocate_hypervisor_per_device = True
self._memory_usage_limit_per_hypervisor = 1024
self._allocate_hypervisor_per_ios_image = True
config = Config.instance()
dynamips_config = config.get_section_config("DYNAMIPS")
self._hypervisor_start_port_range = dynamips_config.get("hypervisor_start_port_range", 7200)
self._hypervisor_end_port_range = dynamips_config.get("hypervisor_end_port_range", 7700)
self._console_start_port_range = dynamips_config.get("console_start_port_range", 2001)
self._console_end_port_range = dynamips_config.get("console_end_port_range", 2500)
self._aux_start_port_range = dynamips_config.get("aux_start_port_range", 2501)
self._aux_end_port_range = dynamips_config.get("aux_end_port_range", 3000)
self._udp_start_port_range = dynamips_config.get("udp_start_port_range", 10001)
self._udp_end_port_range = dynamips_config.get("udp_end_port_range", 20000)
self._ghost_ios_support = dynamips_config.get("ghost_ios_support", True)
self._mmap_support = dynamips_config.get("mmap_support", True)
self._jit_sharing_support = dynamips_config.get("jit_sharing_support", False)
self._sparse_memory_support = dynamips_config.get("sparse_memory_support", True)
self._allocate_hypervisor_per_device = dynamips_config.get("allocate_hypervisor_per_device", True)
self._memory_usage_limit_per_hypervisor = dynamips_config.get("memory_usage_limit_per_hypervisor", 1024)
self._allocate_hypervisor_per_ios_image = dynamips_config.get("allocate_hypervisor_per_ios_image", True)
def __del__(self):
"""
@@ -504,13 +504,12 @@ class HypervisorManager(object):
else:
log.info("allocating an hypervisor per IOS image disabled")
def wait_for_hypervisor(self, host, port, timeout=10):
def wait_for_hypervisor(self, host, port):
"""
Waits for an hypervisor to be started (accepting a socket connection)
:param host: host/address to connect to the hypervisor
:param port: port to connect to the hypervisor
:param timeout: timeout value (default is 10 seconds)
"""
begin = time.time()
+5 -3
View File
@@ -57,6 +57,8 @@ class NIO(object):
Deletes this NIO.
"""
if self._input_filter or self._output_filter:
self.unbind_filter("both")
self._hypervisor.send("nio delete {}".format(self._name))
log.info("NIO {name} has been deleted".format(name=self._name))
@@ -134,7 +136,7 @@ class NIO(object):
def setup_filter(self, direction, options):
"""
Setups a packet filter binded with this NIO.
Setups a packet filter bound with this NIO.
Filter "freq_drop" has 1 argument "<frequency>". It will drop
everything with a -1 frequency, drop every Nth packet with a
@@ -174,7 +176,7 @@ class NIO(object):
:returns: tuple (filter name, filter options)
"""
return (self._input_filter, self._input_filter_options)
return self._input_filter, self._input_filter_options
@property
def output_filter(self):
@@ -184,7 +186,7 @@ class NIO(object):
:returns: tuple (filter name, filter options)
"""
return (self._output_filter, self._output_filter_options)
return self._output_filter, self._output_filter_options
def get_stats(self):
"""
@@ -33,6 +33,7 @@ class ATMBridge(object):
def __init__(self, hypervisor, name):
#FIXME: instance tracking
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atm_bridge create {}".format(self._name))
+66 -23
View File
@@ -20,6 +20,7 @@ Interface for Dynamips virtual ATM switch module ("atmsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class ATMSwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = ATMSwitch._instance_count
ATMSwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "ATM" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atmsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class ATMSwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class ATMSwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class ATMSwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class ATMSwitch(object):
log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def has_port(self, port):
"""
@@ -360,3 +352,54 @@ class ATMSwitch(object):
vpi2=vpi2,
vci2=vci2))
del self._mapping[(port1, vpi1, vci1)]
def start_capture(self, port, output_file, data_link_type="DLT_ATM_RFC1483"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("ATM switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("ATM switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
@@ -29,12 +29,9 @@ class Bridge(object):
:param name: name for this bridge
"""
_allocated_names = []
def __init__(self, hypervisor, name):
self._hypervisor = hypervisor
self._allocated_names.append(name)
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("nio_bridge create {}".format(self._name))
self._hypervisor.devices.append(self)
@@ -58,14 +55,11 @@ class Bridge(object):
:param new_name: New name for this bridge
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -103,7 +97,6 @@ class Bridge(object):
self._hypervisor.send("nio_bridge delete {}".format(self._name))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
def add_nio(self, nio):
"""
+6 -3
View File
@@ -34,13 +34,14 @@ class C1700(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
1720, 1721, 1750, 1751 or 1760 (default = 1720).
1710 is not supported.
"""
def __init__(self, hypervisor, name=None, chassis="1720"):
Router.__init__(self, hypervisor, name, platform="c1700")
def __init__(self, hypervisor, name, router_id=None, chassis="1720"):
Router.__init__(self, hypervisor, name, router_id, platform="c1700")
# Set default values for this platform
self._ram = 64
@@ -50,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
@@ -71,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)
+6 -3
View File
@@ -36,6 +36,7 @@ class C2600(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM (default = 2610).
@@ -54,8 +55,8 @@ class C2600(Router):
"2650XM": C2600_MB_1FE,
"2651XM": C2600_MB_2FE}
def __init__(self, hypervisor, name=None, chassis="2610"):
Router.__init__(self, hypervisor, name, platform="c2600")
def __init__(self, hypervisor, name, router_id=None, chassis="2610"):
Router.__init__(self, hypervisor, name, router_id, platform="c2600")
# Set default values for this platform
self._ram = 64
@@ -65,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
@@ -86,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)
+3 -2
View File
@@ -33,10 +33,11 @@ class C2691(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c2691")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c2691")
# Set default values for this platform
self._ram = 128
+3 -2
View File
@@ -33,12 +33,13 @@ class C3600(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router:
3620, 3640 or 3660 (default = 3640).
"""
def __init__(self, hypervisor, name=None, chassis="3640"):
Router.__init__(self, hypervisor, name, platform="c3600")
def __init__(self, hypervisor, name, router_id=None, chassis="3640"):
Router.__init__(self, hypervisor, name, router_id, platform="c3600")
# Set default values for this platform
self._ram = 128
+3 -2
View File
@@ -33,10 +33,11 @@ class C3725(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c3725")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c3725")
# Set default values for this platform
self._ram = 128
+3 -2
View File
@@ -33,10 +33,11 @@ class C3745(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
"""
def __init__(self, hypervisor, name=None):
Router.__init__(self, hypervisor, name, platform="c3745")
def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, router_id, platform="c3745")
# Set default values for this platform
self._ram = 128
+3 -2
View File
@@ -35,11 +35,12 @@ class C7200(Router):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param npe: default NPE
"""
def __init__(self, hypervisor, name=None, npe="npe-400"):
Router.__init__(self, hypervisor, name, platform="c7200")
def __init__(self, hypervisor, name, router_id=None, npe="npe-400"):
Router.__init__(self, hypervisor, name, router_id, platform="c7200")
# Set default values for this platform
self._ram = 256
@@ -20,6 +20,7 @@ Interface for Dynamips virtual Ethernet switch module ("ethsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class EthernetSwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = EthernetSwitch._instance_count
EthernetSwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "SW" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("ethsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class EthernetSwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class EthernetSwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class EthernetSwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class EthernetSwitch(object):
log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def add_nio(self, nio, port):
"""
@@ -296,3 +288,54 @@ class EthernetSwitch(object):
"""
self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name))
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Ethernet switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
@@ -20,6 +20,7 @@ Interface for Dynamips virtual Frame-Relay switch module.
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642
"""
import os
from ..dynamips_error import DynamipsError
import logging
@@ -34,26 +35,21 @@ class FrameRelaySwitch(object):
:param name: name for this switch
"""
_allocated_names = []
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name=None):
def __init__(self, hypervisor, name):
# create an unique ID
self._id = FrameRelaySwitch._instance_count
FrameRelaySwitch._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "FR" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("frsw create {}".format(self._name))
@@ -68,11 +64,10 @@ class FrameRelaySwitch(object):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -102,7 +97,6 @@ class FrameRelaySwitch(object):
:param new_name: New name for this switch
"""
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -111,9 +105,7 @@ class FrameRelaySwitch(object):
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def hypervisor(self):
@@ -164,7 +156,7 @@ class FrameRelaySwitch(object):
log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
self._instances.remove(self._id)
def has_port(self, port):
"""
@@ -282,3 +274,54 @@ class FrameRelaySwitch(object):
port2=port2,
dlci2=dlci2))
del self._mapping[(port1, dlci1)]
def start_capture(self, port, output_file, data_link_type="DLT_FRELAY"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Frame relay switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Frame relay switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
+65 -16
View File
@@ -19,6 +19,7 @@
Hub object that uses the Bridge interface to create a hub with ports.
"""
import os
from .bridge import Bridge
from ..dynamips_error import DynamipsError
@@ -34,23 +35,20 @@ class Hub(Bridge):
:param name: name for this hub
"""
_instance_count = 1
_instances = []
def __init__(self, hypervisor, name):
# create an unique ID
self._id = Hub._instance_count
Hub._instance_count += 1
# find an instance identifier (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "Hub" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
self._mapping = {}
Bridge.__init__(self, hypervisor, name)
@@ -61,11 +59,10 @@ class Hub(Bridge):
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
@property
def id(self):
@@ -95,6 +92,7 @@ class Hub(Bridge):
Bridge.delete(self)
log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
self._instances.remove(self._id)
def add_nio(self, nio, port):
"""
@@ -137,3 +135,54 @@ class Hub(Bridge):
del self._mapping[port]
return nio
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet hub {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
nio.unbind_filter("both")
log.info("Ethernet hub {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
+130 -56
View File
@@ -37,41 +37,45 @@ class Router(object):
:param hypervisor: Dynamips hypervisor instance
:param name: name for this router
:param router_id: router instance ID
:param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700
:param ghost_flag: used when creating a ghost IOS.
"""
_allocated_names = []
_instances = []
_allocated_console_ports = []
_allocated_aux_ports = []
_instance_count = 1
_status = {0: "inactive",
1: "shutting down",
2: "running",
3: "suspended"}
def __init__(self, hypervisor, name=None, platform="c7200", ghost_flag=False):
def __init__(self, hypervisor, name, router_id=None, platform="c7200", ghost_flag=False):
if not ghost_flag:
# create an unique ID
self._id = Router._instance_count
Router._instance_count += 1
# let's create a unique name if none has been chosen
if not name:
name_id = self._id
while True:
name = "R" + str(name_id)
# check if the name has already been allocated to another router
if name not in self._allocated_names:
if not router_id:
# find an instance identifier if none is provided (0 < id <= 4096)
self._id = 0
for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
else:
if router_id in self._instances:
raise DynamipsError("Router identifier {} is already used by another router".format(router_id))
self._id = router_id
self._instances.append(self._id)
else:
log.info("creating a new ghost IOS file")
self._id = 0
name = "Ghost"
self._allocated_names.append(name)
self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces
self._platform = platform
@@ -137,18 +141,17 @@ class Router(object):
# get the default base MAC address
self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform,
name=self._name))[0]
name=self._name))[0]
self._hypervisor.devices.append(self)
@classmethod
def reset(cls):
"""
Resets the instance count and the allocated names list.
Resets the instance count and the allocated instances list.
"""
cls._instance_count = 1
cls._allocated_names.clear()
cls._instances.clear()
cls._allocated_console_ports.clear()
cls._allocated_aux_ports.clear()
@@ -222,42 +225,32 @@ class Router(object):
:param new_name: new name string
"""
if new_name in self._allocated_names:
raise DynamipsError('Name "{}" is already used by another router'.format(new_name))
if self._startup_config:
# change the hostname in the startup-config
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name))
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id))
if os.path.isfile(startup_config_path):
try:
with open(startup_config_path, "r+") as f:
with open(startup_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
new_startup_config_path = os.path.join(os.path.dirname(startup_config_path), "{}.cfg".format(new_name))
os.rename(startup_config_path, new_startup_config_path)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e))
self.set_config(new_startup_config_path)
if self._private_config:
# change the hostname in the startup-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name))
# change the hostname in the private-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id))
if os.path.isfile(private_config_path):
try:
with open(private_config_path, "r+") as f:
with open(private_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
new_private_config_path = os.path.join(os.path.dirname(private_config_path), "{}-private.cfg".format(new_name))
os.rename(private_config_path, new_private_config_path)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e))
self.set_config(self.startup_config, new_private_config_path)
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name,
new_name=new_name))
@@ -265,10 +258,7 @@ class Router(object):
log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property
def platform(self):
@@ -316,9 +306,9 @@ class Router(object):
self._hypervisor.send("vm delete {}".format(self._name))
self._hypervisor.devices.remove(self)
log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id))
self._allocated_names.remove(self.name)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
if self.aux:
@@ -345,7 +335,8 @@ class Router(object):
os.remove(private_config_path)
log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id))
self._allocated_names.remove(self.name)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
if self.aux:
@@ -374,7 +365,7 @@ class Router(object):
# IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
if elf_header_start != b'\x7fELF\x01\x02\x01':
raise DynamipsError("'{}' is not a valid IOU image".format(self._image))
raise DynamipsError("'{}' is not a valid IOS image".format(self._image))
self._hypervisor.send("vm start {}".format(self._name))
log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id))
@@ -581,10 +572,10 @@ class Router(object):
reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:]
except IOError:
#for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file.
return (None, None)
return None, None
startup_config = reply[0][1:-1] # get statup-config and remove single quotes
private_config = reply[1][1:-1] # get private-config and remove single quotes
return (startup_config, private_config)
return startup_config, private_config
def push_config(self, startup_config, private_config='(keep)'):
"""
@@ -727,7 +718,7 @@ class Router(object):
else:
flag = 0
self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name,
sparsemem=flag))
sparsemem=flag))
if sparsemem:
log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name,
@@ -783,6 +774,9 @@ class Router(object):
:param idlepc: idlepc value (string)
"""
if not idlepc:
idlepc = "0x0"
if not self.is_running():
# router is not running
self._hypervisor.send("vm set_idle_pc {name} {idlepc}".format(name=self._name,
@@ -987,7 +981,7 @@ class Router(object):
translated by the JIT (they contain the native code
corresponding to MIPS code pages).
:param excec_area: exec area value (integer)
:param exec_area: exec area value (integer)
"""
self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name,
@@ -1261,6 +1255,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.
@@ -1268,7 +1271,7 @@ class Router(object):
:returns: slot bindings (adapter names) list
"""
return (self._hypervisor.send("vm slot_bindings {}".format(self._name)))
return self._hypervisor.send("vm slot_bindings {}".format(self._name))
def slot_add_binding(self, slot_id, adapter):
"""
@@ -1284,16 +1287,16 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if slot != None:
if slot is not None:
current_adapter = slot
raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name,
slot_id=slot_id,
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' \
and not (self._platform == 'c3600' and self.chassis == '3660') \
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
if self.is_running() and not (self._platform == '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,
name=self._name))
@@ -1331,14 +1334,14 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if adapter == None:
if adapter is None:
raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name,
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' \
and not (self._platform == 'c3600' and self.chassis == '3660') \
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
if self.is_running() and not (self._platform == '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,
name=self._name))
@@ -1424,8 +1427,8 @@ class Router(object):
# WIC1 = 16, WIC2 = 32 and WIC3 = 48
internal_wic_slot_id = 16 * (wic_slot_id + 1)
self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name,
slot_id=slot_id,
wic_slot_id=internal_wic_slot_id))
slot_id=slot_id,
wic_slot_id=internal_wic_slot_id))
log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name,
id=self._id,
@@ -1548,6 +1551,77 @@ class Router(object):
slot_id=slot_id,
port_id=port_id))
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param slot_id: slot ID
:param port_id: port ID
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
nio = adapter.get_nio(port_id)
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter,
port_id=port_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info("router {name} [id={id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def stop_capture(self, slot_id, port_id):
"""
Stops a packet capture.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
nio.unbind_filter("both")
log.info("router {name} [id={id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def _create_slots(self, numslots):
"""
Creates the appropriate number of slots for this router.
+178 -121
View File
@@ -27,6 +27,7 @@ ATMSW_CREATE_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["name"]
}
ATMSW_DELETE_SCHEMA = {
@@ -86,129 +87,129 @@ ATMSW_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -263,3 +264,59 @@ ATMSW_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "port"]
}
ATMSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ATMSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+178 -121
View File
@@ -27,6 +27,7 @@ ETHHUB_CREATE_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["name"]
}
ETHHUB_DELETE_SCHEMA = {
@@ -86,129 +87,129 @@ ETHHUB_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -260,3 +261,59 @@ ETHHUB_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "port"]
}
ETHHUB_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHHUB_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+179 -122
View File
@@ -27,6 +27,7 @@ ETHSW_CREATE_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["name"]
}
ETHSW_DELETE_SCHEMA = {
@@ -101,129 +102,129 @@ ETHSW_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -268,7 +269,7 @@ ETHSW_ADD_NIO_SCHEMA = {
"dependencies": {
"port_type": ["vlan"],
"vlan": ["port_type"]
}
}
}
ETHSW_DELETE_NIO_SCHEMA = {
@@ -289,3 +290,59 @@ ETHSW_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "port"]
}
ETHSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+178 -121
View File
@@ -27,6 +27,7 @@ FRSW_CREATE_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["name"]
}
FRSW_DELETE_SCHEMA = {
@@ -86,129 +87,129 @@ FRSW_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -263,3 +264,59 @@ FRSW_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "port"]
}
FRSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
FRSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}
+226 -125
View File
@@ -25,6 +25,10 @@ VM_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"router_id": {
"description": "VM/router instance ID",
"type": "integer"
},
"platform": {
"description": "router platform",
"type": "string",
@@ -35,7 +39,7 @@ VM_CREATE_SCHEMA = {
"description": "router chassis model",
"type": "string",
"minLength": 1,
"pattern": "^[0-9]{4}$"
"pattern": "^[0-9]{4}(XM)?$"
},
"image": {
"description": "path to the IOS image file",
@@ -66,7 +70,7 @@ VM_CREATE_SCHEMA = {
}
},
"additionalProperties": False,
"required": ["platform", "image", "ram"]
"required": ["name", "platform", "image", "ram"]
}
VM_DELETE_SCHEMA = {
@@ -198,8 +202,7 @@ VM_UPDATE_SCHEMA = {
"idlepc": {
"description": "idle-pc value",
"type": "string",
"minLength": 1,
"pattern": "^0x[0-9a-fA-F]+$"
"pattern": "^(0x[0-9a-fA-F]+)?$"
},
"idlemax": {
"description": "idlemax value",
@@ -368,6 +371,76 @@ VM_UPDATE_SCHEMA = {
"required": ["id"]
}
VM_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "capture_file_name"]
}
VM_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port"]
}
VM_SAVE_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to save the configs for VM instance",
@@ -382,6 +455,20 @@ VM_SAVE_CONFIG_SCHEMA = {
"required": ["id"]
}
VM_EXPORT_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to export the configs for VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_IDLEPCS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to calculate or show idle-pcs for VM instance",
@@ -400,6 +487,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",
@@ -424,129 +525,129 @@ VM_ADD_NIO_SCHEMA = {
"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
"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": {
+168 -70
View File
@@ -20,17 +20,13 @@ IOU server module.
"""
import os
import sys
import base64
import tempfile
import fcntl
import struct
import socket
import shutil
from gns3server.modules import IModule
from gns3server.config import Config
import gns3server.jsonrpc as jsonrpc
from .iou_device import IOUDevice
from .iou_error import IOUError
from .nios.nio_udp import NIO_UDP
@@ -48,6 +44,9 @@ from .schemas import IOU_RELOAD_SCHEMA
from .schemas import IOU_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import IOU_ADD_NIO_SCHEMA
from .schemas import IOU_DELETE_NIO_SCHEMA
from .schemas import IOU_START_CAPTURE_SCHEMA
from .schemas import IOU_STOP_CAPTURE_SCHEMA
from .schemas import IOU_EXPORT_CONFIG_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -67,9 +66,9 @@ class IOU(IModule):
# get the iouyap location
config = Config.instance()
iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap")
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,12 +86,12 @@ class IOU(IModule):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._iou_instances = {}
self._console_start_port_range = 4001
self._console_end_port_range = 4512
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", 4500)
self._allocated_udp_ports = []
self._udp_start_port_range = 30001
self._udp_end_port_range = 40001
self._host = kwargs["host"]
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", 35000)
self._host = iou_config.get("host", kwargs["host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
@@ -215,12 +214,12 @@ class IOU(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
return
if "iourc" in request:
iourc_content = base64.decodestring(request["iourc"].encode("utf-8")).decode("utf-8")
iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8")
iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix
try:
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
@@ -228,7 +227,7 @@ class IOU(IModule):
f.write(iourc_content)
self._iourc = f.name
except OSError as e:
raise IOUError("Could not save iourc file to {}: {}".format(f.name, e))
raise IOUError("Could not create the iourc file: {}".format(e))
if "iouyap" in request and request["iouyap"]:
self._iouyap = request["iouyap"]
@@ -255,7 +254,7 @@ class IOU(IModule):
self._working_dir = new_working_dir
for iou_id in self._iou_instances:
iou_instance = self._iou_instances[iou_id]
iou_instance.working_dir = self._working_dir
iou_instance.working_dir = os.path.join(self._working_dir, "iou", "device-{}".format(iou_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"]
@@ -267,30 +266,6 @@ class IOU(IModule):
log.debug("received request {}".format(request))
def test_result(self, message, result="error"):
"""
"""
return {"result": result, "message": message}
@IModule.route("iou.test_settings")
def test_settings(self, request):
"""
"""
response = []
# test iourc
if self._iourc == "":
response.append(self.test_result("No iourc file has been added"))
elif not os.path.isfile(self._iourc):
response.append(self.test_result("iourc file {} is not accessible".format(self._iourc)))
else:
#TODO: check hostname + license inside the file
pass
self.send_response(response)
@IModule.route("iou.create")
def iou_create(self, request):
"""
@@ -315,22 +290,21 @@ class IOU(IModule):
if not self.validate_request(request, IOU_CREATE_SCHEMA):
return
name = request.get("name")
console = request.get("console")
name = request["name"]
iou_path = request["path"]
console = request.get("console")
iou_id = request.get("iou_id")
updated_iou_path = os.path.join(self.images_directory, iou_path)
if os.path.isfile(updated_iou_path):
iou_path = updated_iou_path
try:
try:
os.makedirs(self._working_dir)
except FileExistsError:
pass
except OSError as e:
raise IOUError("Could not create working directory {}".format(e))
iou_instance = IOUDevice(iou_path,
iou_instance = IOUDevice(name,
iou_path,
self._working_dir,
self._host,
name,
iou_id,
console,
self._console_start_port_range,
self._console_end_port_range)
@@ -389,7 +363,7 @@ class IOU(IModule):
Optional request parameters:
- any setting to update
- startup_config_base64 (startup-config base64 encoded)
- initial_config_base64 (initial-config base64 encoded)
Response parameters:
- updated settings
@@ -406,36 +380,36 @@ class IOU(IModule):
if not iou_instance:
return
config_path = os.path.join(iou_instance.working_dir, "startup-config")
config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg")
try:
if "startup_config_base64" in request:
# a new startup-config has been pushed
config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8")
if "initial_config_base64" in request:
# a new initial-config has been pushed
config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name)
try:
with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(config_path))
log.info("saving initial-config to {}".format(config_path))
f.write(config)
except OSError as e:
raise IOUError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local startup-config path
request["startup_config"] = os.path.basename(config_path)
elif "startup_config" in request:
if os.path.isfile(request["startup_config"]) and request["startup_config"] != config_path:
# update the request with the new local initial-config path
request["initial_config"] = os.path.basename(config_path)
elif "initial_config" in request:
if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path:
# this is a local file set in the GUI
try:
with open(request["startup_config"], "r") as f:
with open(request["initial_config"], "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name)
f.write(config)
request["startup_config"] = os.path.basename(config_path)
request["initial_config"] = os.path.basename(config_path)
except OSError as e:
raise IOUError("Could not save the configuration from {} to {}: {}".format(request["startup_config"], config_path, e))
raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e))
elif not os.path.isfile(config_path):
raise IOUError("Startup-config {} could not be found on this server".format(request["startup_config"]))
raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"]))
except IOUError as e:
self.send_custom_error(str(e))
return
@@ -587,8 +561,8 @@ class IOU(IModule):
iou_instance.id,
port,
self._host))
response = {"lport": port}
response["port_id"] = request["port_id"]
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("iou.add_nio")
@@ -643,14 +617,13 @@ class IOU(IModule):
nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"]
if not self.has_privileged_access(self._iouyap, tap_device):
if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device))
nio = NIO_TAP(tap_device)
elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"]
if not self.has_privileged_access(self._iouyap, ethernet_device):
if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device))
self._check_for_privileged_access(ethernet_device)
nio = NIO_GenericEthernet(ethernet_device)
if not nio:
raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
@@ -703,6 +676,131 @@ class IOU(IModule):
self.send_response(True)
@IModule.route("iou.start_capture")
def start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- slot (slot number)
- port (port number)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, IOU_START_CAPTURE_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
slot = request["slot"]
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name)
iou_instance.start_capture(slot, port, capture_file_path, data_link_type)
except IOUError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("iou.stop_capture")
def stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- slot (slot number)
- port (port number)
- port_id (port identifier)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, IOU_STOP_CAPTURE_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
slot = request["slot"]
port = request["port"]
try:
iou_instance.stop_capture(slot, port)
except IOUError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("iou.export_config")
def export_config(self, request):
"""
Exports the initial-config from an IOU instance.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- initial_config_base64 (initial-config base64 encoded)
- False if no configuration can be exported
"""
# validate the request
if not self.validate_request(request, IOU_EXPORT_CONFIG_SCHEMA):
return
# get the instance
iou_instance = self.get_iou_instance(request["id"])
if not iou_instance:
return
if not iou_instance.initial_config:
self.send_custom_error("unable to export the initial-config because it doesn't exist")
return
response = {}
initial_config_path = os.path.join(iou_instance.working_dir, iou_instance.initial_config)
try:
with open(initial_config_path, "rb") as f:
config = f.read()
response["initial_config_base64"] = base64.encodebytes(config).decode("utf-8")
except OSError as e:
self.send_custom_error("unable to export the initial-config: {}".format(e))
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("iou.echo")
def echo(self, request):
"""
@@ -711,7 +809,7 @@ class IOU(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
+171 -72
View File
@@ -46,10 +46,11 @@ class IOUDevice(object):
"""
IOU device implementation.
: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 name: name of this IOU device
:param iou_id: IOU instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
@@ -58,29 +59,34 @@ class IOUDevice(object):
_instances = []
_allocated_console_ports = []
def __init__(self, path,
def __init__(self,
name,
path,
working_dir,
host="127.0.0.1",
name=None,
iou_id = None,
console=None,
console_start_port_range=4001,
console_end_port_range=4512):
# find an instance identifier (0 < id <= 512)
self._id = 0
for identifier in range(1, 513):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if not iou_id:
# find an instance identifier if none is provided (0 < id <= 512)
self._id = 0
for identifier in range(1, 513):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise IOUError("Maximum number of IOU instances reached")
if name:
self._name = name
if self._id == 0:
raise IOUError("Maximum number of IOU instances reached")
else:
self._name = "IOU{}".format(self._id)
if iou_id in self._instances:
raise IOUError("IOU identifier {} is already used by another IOU device".format(iou_id))
self._id = iou_id
self._instances.append(self._id)
self._name = name
self._path = path
self._iourc = ""
self._iouyap = ""
@@ -104,12 +110,17 @@ class IOUDevice(object):
self._slots = self._ethernet_adapters + self._serial_adapters
self._use_default_iou_values = True # for RAM & NVRAM values
self._nvram = 128 # Kilobytes
self._startup_config = ""
self._initial_config = ""
self._ram = 256 # Megabytes
self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes).
# update the working directory
self.working_dir = working_dir
working_dir_path = os.path.join(working_dir, "iou", "device-{}".format(self._id))
if iou_id and not os.path.isdir(working_dir_path):
raise IOUError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
@@ -137,7 +148,7 @@ class IOUDevice(object):
iou_defaults = {"name": self._name,
"path": self._path,
"startup_config": self._startup_config,
"intial_config": self._initial_config,
"use_default_iou_values": self._use_default_iou_values,
"ram": self._ram,
"nvram": self._nvram,
@@ -156,7 +167,7 @@ class IOUDevice(object):
:returns: id (integer)
"""
return(self._id)
return self._id
@classmethod
def reset(cls):
@@ -185,12 +196,12 @@ class IOUDevice(object):
:param new_name: name
"""
if self._startup_config:
# update the startup-config
config_path = os.path.join(self.working_dir, "startup-config")
if self._initial_config:
# update the initial-config
config_path = os.path.join(self._working_dir, "initial-config.cfg")
if os.path.isfile(config_path):
try:
with open(config_path, "r+") as f:
with open(config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self._name, new_name)
f.seek(0)
@@ -211,7 +222,7 @@ class IOUDevice(object):
:returns: path to IOU
"""
return(self._path)
return self._path
@path.setter
def path(self, path):
@@ -223,8 +234,8 @@ class IOUDevice(object):
self._path = path
log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name,
id=self._id,
path=path))
id=self._id,
path=path))
@property
def iourc(self):
@@ -234,14 +245,14 @@ class IOUDevice(object):
:returns: path to the iourc file
"""
return(self._iourc)
return self._iourc
@iourc.setter
def iourc(self, iourc):
"""
Sets the path to the iourc file.
:param path: path to the iourc file.
:param iourc: path to the iourc file.
"""
self._iourc = iourc
@@ -257,14 +268,14 @@ class IOUDevice(object):
:returns: path to iouyap
"""
return(self._iouyap)
return self._iouyap
@iouyap.setter
def iouyap(self, iouyap):
"""
Sets the path to iouyap.
:param path: path to iouyap
:param iouyap: path to iouyap
"""
self._iouyap = iouyap
@@ -290,8 +301,6 @@ class IOUDevice(object):
:param working_dir: path to the working directory
"""
# create our own working directory
working_dir = os.path.join(working_dir, "iou", "device-{}".format(self._id))
try:
os.makedirs(working_dir)
except FileExistsError:
@@ -301,8 +310,8 @@ class IOUDevice(object):
self._working_dir = working_dir
log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
id=self._id,
wd=self._working_dir))
@property
def console(self):
@@ -329,8 +338,8 @@ class IOUDevice(object):
self._console = console
self._allocated_console_ports.append(self._console)
log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
id=self._id,
port=console))
def command(self):
"""
@@ -347,9 +356,10 @@ class IOUDevice(object):
"""
self.stop()
self._instances.remove(self._id)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
if self.console and self.console in self._allocated_console_ports:
self._allocated_console_ports.remove(self.console)
log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name,
@@ -357,11 +367,12 @@ class IOUDevice(object):
def clean_delete(self):
"""
Deletes this IOU device & all files (nvram, startup-config etc.)
Deletes this IOU device & all files (nvram, initial-config etc.)
"""
self.stop()
self._instances.remove(self._id)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
@@ -370,8 +381,8 @@ class IOUDevice(object):
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete IOU device {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
id=self._id,
error=e))
return
log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
@@ -404,6 +415,7 @@ class IOUDevice(object):
for unit in adapter.ports.keys():
nio = adapter.get_nio(unit)
if nio:
connection = None
if isinstance(nio, NIO_UDP):
# UDP tunnel
connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport,
@@ -417,7 +429,25 @@ class IOUDevice(object):
# Ethernet interface
connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)}
config["{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)] = connection
if connection:
interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)
config[interface] = connection
if nio.capturing:
pcap_data_link_type = nio.pcap_data_link_type.upper()
if pcap_data_link_type == "DLT_PPP_SERIAL":
pcap_protocol = "ppp"
elif pcap_data_link_type == "DLT_C_HDLC":
pcap_protocol = "hdlc"
elif pcap_data_link_type == "DLT_FRELAY":
pcap_protocol = "fr"
else:
pcap_protocol = "ethernet"
capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file),
"pcap_protocol": pcap_protocol,
"pcap_overwrite": "y"}
config[interface].update(capture_info)
unit_id += 1
bay_id += 1
@@ -491,7 +521,7 @@ class IOUDevice(object):
try:
output = subprocess.check_output(["ldd", self._path])
except (subprocess.SubprocessError, FileNotFoundError) as e:
except (FileNotFoundError, subprocess.CalledProcessError) as e:
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
return
@@ -508,7 +538,7 @@ class IOUDevice(object):
if not self.is_running():
if not os.path.isfile(self._path):
if not os.path.isfile(self._path) or not os.path.exists(self._path):
raise IOUError("IOU image '{}' is not accessible".format(self._path))
try:
@@ -583,7 +613,7 @@ class IOUDevice(object):
self._iouyap_process.wait(1)
except subprocess.TimeoutExpired:
self._iouyap_process.kill()
if self._iouyap_process.poll() == None:
if self._iouyap_process.poll() is None:
log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid,
self._id))
self._iouyap_process = None
@@ -596,7 +626,7 @@ class IOUDevice(object):
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() == None:
if self._process.poll() is None:
log.warn("IOU instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
@@ -611,7 +641,7 @@ class IOUDevice(object):
output = ""
if self._iou_stdout_file:
try:
with open(self._iou_stdout_file) as file:
with open(self._iou_stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._iou_stdout_file, e))
@@ -626,7 +656,7 @@ class IOUDevice(object):
output = ""
if self._iouyap_stdout_file:
try:
with open(self._iouyap_stdout_file) as file:
with open(self._iouyap_stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e))
@@ -639,7 +669,7 @@ class IOUDevice(object):
:returns: True or False
"""
if self._process and self._process.poll() == None:
if self._process and self._process.poll() is None:
return True
return False
@@ -650,7 +680,7 @@ class IOUDevice(object):
:returns: True or False
"""
if self._iouyap_process and self._iouyap_process.poll() == None:
if self._iouyap_process and self._iouyap_process.poll() is None:
return True
return False
@@ -725,16 +755,14 @@ class IOUDevice(object):
env = os.environ.copy()
env["IOURC"] = self._iourc
output = b""
try:
output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env)
except OSError as e:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
else:
if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")):
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:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
def _build_command(self):
"""
@@ -766,7 +794,6 @@ class IOUDevice(object):
-N Ignore the NETMAP file
"""
#TODO: add support for keepalive and watchdog
command = [self._path]
if len(self._ethernet_adapters) != 2:
command.extend(["-e", str(len(self._ethernet_adapters))])
@@ -776,8 +803,8 @@ class IOUDevice(object):
command.extend(["-n", str(self._nvram)])
command.extend(["-m", str(self._ram)])
command.extend(["-L"]) # disable local console, use remote console
if self._startup_config:
command.extend(["-c", self._startup_config])
if self._initial_config:
command.extend(["-c", self._initial_config])
if self._l1_keepalives:
self._enable_l1_keepalives(command)
command.extend([str(self._id)])
@@ -887,27 +914,27 @@ class IOUDevice(object):
self._nvram = nvram
@property
def startup_config(self):
def initial_config(self):
"""
Returns the startup-config for this IOU instance.
Returns the initial-config for this IOU instance.
:returns: path to startup-config file
:returns: path to initial-config file
"""
return self._startup_config
return self._initial_config
@startup_config.setter
def startup_config(self, startup_config):
@initial_config.setter
def initial_config(self, initial_config):
"""
Sets the startup-config for this IOU instance.
Sets the initial-config for this IOU instance.
:param startup_config: path to startup-config file
:param initial_config: path to initial-config file
"""
self._startup_config = startup_config
log.info("IOU {name} [id={id}]: startup_config set to {config}".format(name=self._name,
id=self._id,
config=self._startup_config))
self._initial_config = initial_config
log.info("IOU {name} [id={id}]: initial_config set to {config}".format(name=self._name,
id=self._id,
config=self._initial_config))
@property
def ethernet_adapters(self):
@@ -964,3 +991,75 @@ class IOUDevice(object):
adapters=len(self._serial_adapters)))
self._slots = self._ethernet_adapters + self._serial_adapters
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param slot_id: slot ID
:param port_id: port ID
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
if nio.capturing:
raise IOUError("Packet capture is already activated on {slot_id}/{port_id}".format(slot_id=slot_id,
port_id=port_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise IOUError("Could not create captures directory {}".format(e))
nio.startPacketCapture(output_file, data_link_type)
log.info("IOU {name} [id={id}]: starting packet capture on {slot_id}/{port_id}".format(name=self._name,
id=self._id,
slot_id=slot_id,
port_id=port_id))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
def stop_capture(self, slot_id, port_id):
"""
Stops a packet capture.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
nio.stopPacketCapture()
log.info("IOU {name} [id={id}]: stopping packet capture on {slot_id}/{port_id}".format(name=self._name,
id=self._id,
slot_id=slot_id,
port_id=port_id))
if self.is_iouyap_running():
self._update_iouyap_config()
os.kill(self._iouyap_process.pid, signal.SIGHUP)
+31 -30
View File
@@ -56,32 +56,32 @@ EXIT_ABORT = 2
# Mostly from:
# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
#--[ Telnet Commands ]---------------------------------------------------------
SE = 240 # End of subnegotiation 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
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
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
@@ -299,9 +299,7 @@ class TelnetServer(Console):
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]):
if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]:
self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else:
@@ -326,7 +324,7 @@ class TelnetServer(Console):
fd.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
IAC, DO, BINARY]))
if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
fd.send(b'\r\nToo many connections\r\n')
@@ -381,7 +379,10 @@ class IOU(Router):
return buf
def write(self, buf):
self.fd.send(buf)
try:
self.fd.send(buf)
except BlockingIOError:
return
def _open(self):
self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
@@ -601,7 +602,7 @@ def start_ioucon(cmdline_args, stop_event):
nport = int(port)
except ValueError:
pass
if (addr == '' or nport == 0):
if addr == '' or nport == 0:
raise ConfigError('format for --telnet-server must be '
'ADDR:PORT (like 127.0.0.1:20000)')
+79
View File
@@ -0,0 +1,79 @@
# -*- 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 = ""
self._pcap_data_link_type = ""
def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"):
"""
:param pcap_output_file: PCAP destination file for the capture
:param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
self._pcap_data_link_type = pcap_data_link_type
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
self._pcap_data_link_type = ""
@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
@property
def pcap_data_link_type(self):
"""
Returns the PCAP data link type
:returns: PCAP data link type (DLT_* value)
"""
return self._pcap_data_link_type
@@ -19,16 +19,19 @@
Interface for generic Ethernet NIOs (PCAP library).
"""
from .nio import NIO
class NIO_GenericEthernet(object):
class NIO_GenericEthernet(NIO):
"""
NIO generic Ethernet NIO.
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
def __init__(self, ethernet_device):
NIO.__init__(self)
self._ethernet_device = ethernet_device
@property
+5 -2
View File
@@ -19,16 +19,19 @@
Interface for TAP NIOs (UNIX based OSes only).
"""
from .nio import NIO
class NIO_TAP(object):
class NIO_TAP(NIO):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""
def __init__(self, tap_device):
NIO.__init__(self)
self._tap_device = tap_device
@property
+5 -2
View File
@@ -19,10 +19,12 @@
Interface for UDP NIOs.
"""
from .nio import NIO
class NIO_UDP(object):
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
@@ -33,6 +35,7 @@ class NIO_UDP(object):
def __init__(self, lport, rhost, rport):
NIO.__init__(self)
self._lport = lport
self._rhost = rhost
self._rport = rport
+214 -126
View File
@@ -26,6 +26,10 @@ IOU_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"iou_id": {
"description": "IOU device instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
@@ -39,7 +43,7 @@ IOU_CREATE_SCHEMA = {
}
},
"additionalProperties": False,
"required": ["path"],
"required": ["name", "path"],
}
IOU_DELETE_SCHEMA = {
@@ -75,8 +79,8 @@ IOU_UPDATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"startup_config": {
"description": "path to the IOU startup configuration file",
"initial_config": {
"description": "path to the IOU initial configuration file",
"type": "string",
"minLength": 1,
},
@@ -114,8 +118,8 @@ IOU_UPDATE_SCHEMA = {
"description": "enable or disable layer 1 keepalive messages",
"type": "boolean"
},
"startup_config_base64": {
"description": "startup configuration base64 encoded",
"initial_config_base64": {
"description": "initial configuration base64 encoded",
"type": "string"
},
},
@@ -189,129 +193,129 @@ IOU_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -378,3 +382,87 @@ IOU_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "slot", "port"]
}
IOU_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an IOU instance port",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "slot", "port", "port_id", "capture_file_name"]
}
IOU_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an IOU instance port",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 15
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"port_id": {
"description": "Unique port identifier for the IOU instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "slot", "port", "port_id"]
}
IOU_EXPORT_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to export an initial-config from an IOU instance",
"type": "object",
"properties": {
"id": {
"description": "IOU device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
+660
View File
@@ -0,0 +1,660 @@
# -*- 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._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()
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_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("QEMU emulator version ([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 (OSError, subprocess.CalledProcessError) 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:
- Server address/host
- 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
paths.append(os.path.join(os.getcwd(), "qemu"))
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 a specific location on Mac OS X regardless of what's in $PATH
paths.append("/usr/local/bin")
for path in paths:
try:
for f in os.listdir(path):
if f.startswith("qemu-system") and os.access(os.path.join(path, f), os.X_OK):
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 = {"server": self._host,
"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)
Whitespace-only changes.
+104
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
@@ -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"
Whitespace-only changes.
+65
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
+75
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"
+39
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
+785
View File
@@ -0,0 +1,785 @@
# -*- 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 VM instance.
"""
import os
import shutil
import random
import subprocess
import shlex
from .qemu_error import QemuError
from .adapters.ethernet_adapter import EthernetAdapter
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
import logging
log = logging.getLogger(__name__)
class QemuVM(object):
"""
QEMU VM implementation.
:param name: name of this QEMU VM
:param qemu_path: path to the QEMU binary
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param qemu_id: QEMU VM instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
"""
_instances = []
_allocated_console_ports = []
def __init__(self,
name,
qemu_path,
working_dir,
host="127.0.0.1",
qemu_id=None,
console=None,
console_start_port_range=5001,
console_end_port_range=5500):
if not qemu_id:
self._id = 0
for identifier in range(1, 1024):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise QemuError("Maximum number of QEMU VM instances reached")
else:
if qemu_id in self._instances:
raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id))
self._id = qemu_id
self._instances.append(self._id)
self._name = name
self._working_dir = None
self._host = host
self._command = []
self._started = False
self._process = None
self._stdout_file = ""
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# QEMU settings
self._qemu_path = qemu_path
self._hda_disk_image = ""
self._hdb_disk_image = ""
self._options = ""
self._ram = 256
self._console = console
self._ethernet_adapters = []
self._adapter_type = "e1000"
self._initrd = ""
self._kernel_image = ""
self._kernel_command_line = ""
working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id))
if qemu_id and not os.path.isdir(working_dir_path):
raise QemuError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise QemuError(e)
if self._console in self._allocated_console_ports:
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
self._allocated_console_ports.append(self._console)
self.adapters = 1 # creates 1 adapter by default
log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name,
id=self._id))
def defaults(self):
"""
Returns all the default attribute values for this QEMU VM.
:returns: default values (dictionary)
"""
qemu_defaults = {"name": self._name,
"qemu_path": self._qemu_path,
"ram": self._ram,
"hda_disk_image": self._hda_disk_image,
"hdb_disk_image": self._hdb_disk_image,
"options": self._options,
"adapters": self.adapters,
"adapter_type": self._adapter_type,
"console": self._console,
"initrd": self._initrd,
"kernel_image": self._kernel_image,
"kernel_command_line": self._kernel_command_line}
return qemu_defaults
@property
def id(self):
"""
Returns the unique ID for this QEMU VM.
:returns: id (integer)
"""
return self._id
@classmethod
def reset(cls):
"""
Resets allocated instance list.
"""
cls._instances.clear()
cls._allocated_console_ports.clear()
@property
def name(self):
"""
Returns the name of this QEMU VM.
:returns: name
"""
return self._name
@name.setter
def name(self, new_name):
"""
Sets the name of this QEMU VM.
:param new_name: name
"""
log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@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 this QEMU VM.
:param working_dir: path to the working directory
"""
try:
os.makedirs(working_dir)
except FileExistsError:
pass
except OSError as e:
raise QemuError("Could not create working directory {}: {}".format(working_dir, e))
self._working_dir = working_dir
log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
@property
def console(self):
"""
Returns the TCP console port.
:returns: console port (integer)
"""
return self._console
@console.setter
def console(self, console):
"""
Sets the TCP console port.
:param console: console port (integer)
"""
if console in self._allocated_console_ports:
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
def delete(self):
"""
Deletes this QEMU VM.
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console and self.console in self._allocated_console_ports:
self._allocated_console_ports.remove(self.console)
log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
def clean_delete(self):
"""
Deletes this QEMU VM & all files.
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property
def qemu_path(self):
"""
Returns the QEMU binary path for this QEMU VM.
:returns: QEMU path
"""
return self._qemu_path
@qemu_path.setter
def qemu_path(self, qemu_path):
"""
Sets the QEMU binary path this QEMU VM.
:param qemu_path: QEMU path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name,
id=self._id,
qemu_path=qemu_path))
self._qemu_path = qemu_path
@property
def hda_disk_image(self):
"""
Returns the hda disk image path for this QEMU VM.
:returns: QEMU hda disk image path
"""
return self._hda_disk_image
@hda_disk_image.setter
def hda_disk_image(self, hda_disk_image):
"""
Sets the hda disk image for this QEMU VM.
:param hda_disk_image: QEMU hda disk image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name,
id=self._id,
disk_image=hda_disk_image))
self._hda_disk_image = hda_disk_image
@property
def hdb_disk_image(self):
"""
Returns the hdb disk image path for this QEMU VM.
:returns: QEMU hdb disk image path
"""
return self._hdb_disk_image
@hdb_disk_image.setter
def hdb_disk_image(self, hdb_disk_image):
"""
Sets the hdb disk image for this QEMU VM.
:param hdb_disk_image: QEMU hdb disk image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name,
id=self._id,
disk_image=hdb_disk_image))
self._hdb_disk_image = hdb_disk_image
@property
def adapters(self):
"""
Returns the number of Ethernet adapters for this QEMU VM instance.
:returns: number of adapters
"""
return len(self._ethernet_adapters)
@adapters.setter
def adapters(self, adapters):
"""
Sets the number of Ethernet adapters for this QEMU VM instance.
:param adapters: number of adapters
"""
self._ethernet_adapters.clear()
for adapter_id in range(0, adapters):
self._ethernet_adapters.append(EthernetAdapter())
log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
id=self._id,
adapters=adapters))
@property
def adapter_type(self):
"""
Returns the adapter type for this QEMU VM instance.
:returns: adapter type (string)
"""
return self._adapter_type
@adapter_type.setter
def adapter_type(self, adapter_type):
"""
Sets the adapter type for this QEMU VM instance.
:param adapter_type: adapter type (string)
"""
self._adapter_type = adapter_type
log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
id=self._id,
adapter_type=adapter_type))
@property
def ram(self):
"""
Returns the RAM amount for this QEMU VM.
:returns: RAM amount in MB
"""
return self._ram
@ram.setter
def ram(self, ram):
"""
Sets the amount of RAM for this QEMU VM.
:param ram: RAM amount in MB
"""
log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name,
id=self._id,
ram=ram))
self._ram = ram
@property
def options(self):
"""
Returns the options for this QEMU VM.
:returns: QEMU options
"""
return self._options
@options.setter
def options(self, options):
"""
Sets the options for this QEMU VM.
:param options: QEMU options
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name,
id=self._id,
options=options))
self._options = options
@property
def initrd(self):
"""
Returns the initrd path for this QEMU VM.
:returns: QEMU initrd path
"""
return self._initrd
@initrd.setter
def initrd(self, initrd):
"""
Sets the initrd path for this QEMU VM.
:param initrd: QEMU initrd path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name,
id=self._id,
initrd=initrd))
self._initrd = initrd
@property
def kernel_image(self):
"""
Returns the kernel image path for this QEMU VM.
:returns: QEMU kernel image path
"""
return self._kernel_image
@kernel_image.setter
def kernel_image(self, kernel_image):
"""
Sets the kernel image path for this QEMU VM.
:param kernel_image: QEMU kernel image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name,
id=self._id,
kernel_image=kernel_image))
self._kernel_image = kernel_image
@property
def kernel_command_line(self):
"""
Returns the kernel command line for this QEMU VM.
:returns: QEMU kernel command line
"""
return self._kernel_command_line
@kernel_command_line.setter
def kernel_command_line(self, kernel_command_line):
"""
Sets the kernel command line for this QEMU VM.
:param kernel_command_line: QEMU kernel command line
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name,
id=self._id,
kernel_command_line=kernel_command_line))
self._kernel_command_line = kernel_command_line
def start(self):
"""
Starts this QEMU VM.
"""
if not self.is_running():
if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path):
raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path))
self._command = self._build_command()
try:
log.info("starting QEMU: {}".format(self._command))
self._stdout_file = os.path.join(self._working_dir, "qemu.log")
log.info("logging to {}".format(self._stdout_file))
with open(self._stdout_file, "w") as fd:
self._process = subprocess.Popen(self._command,
stdout=fd,
stderr=subprocess.STDOUT,
cwd=self._working_dir)
log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid))
self._started = True
except OSError as e:
stdout = self.read_stdout()
log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
def stop(self):
"""
Stops this QEMU VM.
"""
# stop the QEMU process
if self.is_running():
log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid))
try:
self._process.terminate()
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() is None:
log.warn("QEMU VM instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
self._started = False
def suspend(self):
"""
Suspends this QEMU VM.
"""
pass
def reload(self):
"""
Reloads this QEMU VM.
"""
pass
def resume(self):
"""
Resumes this QEMU VM.
"""
pass
def port_add_nio_binding(self, adapter_id, nio):
"""
Adds a port NIO binding.
:param adapter_id: adapter ID
:param nio: NIO instance to add to the slot/port
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
adapter_id=adapter_id))
adapter.add_nio(0, nio)
log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
id=self._id,
nio=nio,
adapter_id=adapter_id))
def port_remove_nio_binding(self, adapter_id):
"""
Removes a port NIO binding.
:param adapter_id: adapter ID
:returns: NIO instance
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
adapter_id=adapter_id))
nio = adapter.get_nio(0)
adapter.remove_nio(0)
log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name,
id=self._id,
nio=nio,
adapter_id=adapter_id))
return nio
@property
def started(self):
"""
Returns either this QEMU VM has been started or not.
:returns: boolean
"""
return self._started
def read_stdout(self):
"""
Reads the standard output of the QEMU process.
Only use when the process has been stopped or has crashed.
"""
output = ""
if self._stdout_file:
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 QEMU process is running
:returns: True or False
"""
if self._process and self._process.poll() is None:
return True
return False
def command(self):
"""
Returns the QEMU command line.
:returns: QEMU command line (string)
"""
return " ".join(self._build_command())
def _serial_options(self):
if self._console:
return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)]
else:
return []
def _disk_options(self):
options = []
qemu_img_path = ""
qemu_path_dir = os.path.dirname(self._qemu_path)
try:
for f in os.listdir(qemu_path_dir):
if f.startswith("qemu-img"):
qemu_img_path = os.path.join(qemu_path_dir, f)
except OSError as e:
raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e))
if not qemu_img_path:
raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir))
try:
if self._hda_disk_image:
hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2")
if not os.path.exists(hda_disk):
retcode = subprocess.call([qemu_img_path, "create", "-o",
"backing_file={}".format(self._hda_disk_image),
"-f", "qcow2", hda_disk])
log.info("{} returned with {}".format(qemu_img_path, retcode))
else:
# create a "FLASH" with 256MB if no disk image has been specified
hda_disk = os.path.join(self._working_dir, "flash.qcow2")
if not os.path.exists(hda_disk):
retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M"])
log.info("{} returned with {}".format(qemu_img_path, retcode))
except OSError as e:
raise QemuError("Could not create disk image {}".format(e))
options.extend(["-hda", hda_disk])
if self._hdb_disk_image:
hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2")
if not os.path.exists(hdb_disk):
try:
retcode = subprocess.call([qemu_img_path, "create", "-o",
"backing_file={}".format(self._hdb_disk_image),
"-f", "qcow2", hdb_disk])
log.info("{} returned with {}".format(qemu_img_path, retcode))
except OSError as e:
raise QemuError("Could not create disk image {}".format(e))
options.extend(["-hdb", hdb_disk])
return options
def _linux_boot_options(self):
options = []
if self._initrd:
options.extend(["-initrd", self._initrd])
if self._kernel_image:
options.extend(["-kernel", self._kernel_image])
if self._kernel_command_line:
options.extend(["-append", self._kernel_command_line])
return options
def _network_options(self):
network_options = []
adapter_id = 0
for adapter in self._ethernet_adapters:
#TODO: let users specify a base mac address
mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id)
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)])
nio = adapter.get_nio(0)
if nio and isinstance(nio, NIO_UDP):
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
nio.rhost,
nio.rport,
self._host,
nio.lport)])
else:
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
adapter_id += 1
return network_options
def _build_command(self):
"""
Command to start the QEMU process.
(to be passed to subprocess.Popen())
"""
command = [self._qemu_path]
command.extend(["-name", self._name])
command.extend(["-m", str(self._ram)])
command.extend(self._disk_options())
command.extend(self._linux_boot_options())
command.extend(self._serial_options())
additional_options = self._options.strip()
if additional_options:
command.extend(shlex.split(additional_options))
command.extend(self._network_options())
return command
+388
View File
@@ -0,0 +1,388 @@
# -*- 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": 1,
"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",
},
"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"]
}
+806
View File
@@ -0,0 +1,806 @@
# -*- 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/>.
"""
VirtualBox server module.
"""
import sys
import os
import socket
import shutil
from pkg_resources import parse_version
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
from .schemas import VBOX_CREATE_SCHEMA
from .schemas import VBOX_DELETE_SCHEMA
from .schemas import VBOX_UPDATE_SCHEMA
from .schemas import VBOX_START_SCHEMA
from .schemas import VBOX_STOP_SCHEMA
from .schemas import VBOX_SUSPEND_SCHEMA
from .schemas import VBOX_RELOAD_SCHEMA
from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import VBOX_ADD_NIO_SCHEMA
from .schemas import VBOX_DELETE_NIO_SCHEMA
from .schemas import VBOX_START_CAPTURE_SCHEMA
from .schemas import VBOX_STOP_CAPTURE_SCHEMA
import logging
log = logging.getLogger(__name__)
class VirtualBox(IModule):
"""
VirtualBox module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# get the vboxwrapper location (only non-Windows platforms)
if not sys.platform.startswith("win"):
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(os.pathsep)
# look for vboxwrapper 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
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")
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vbox_instances = {}
config = Config.instance()
vbox_config = config.get_section_config(name.upper())
self._console_start_port_range = vbox_config.get("console_start_port_range", 3501)
self._console_end_port_range = vbox_config.get("console_end_port_range", 4000)
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 = vbox_config.get("host", kwargs["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 pywintypes
import win32com.client
try:
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()
win32com.client.gencache.EnsureDispatch("VirtualBox.VirtualBox")
except pywintypes.com_error:
raise VirtualBoxError("VirtualBox is not installed.")
try:
from .vboxapi_py3 import VirtualBoxManager
self._vboxmanager = VirtualBoxManager(None, None)
vbox_major_version, vbox_minor_version, _ = self._vboxmanager.vbox.version.split('.')
if parse_version("{}.{}".format(vbox_major_version, vbox_minor_version)) <= parse_version("4.1"):
raise VirtualBoxError("VirtualBox version must be >= 4.2")
except Exception as e:
self._vboxmanager = None
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()
try:
self._vboxwrapper.start()
except VirtualBoxError:
self._vboxwrapper = None
raise
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# 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()
IModule.stop(self, signum) # this will stop the I/O loop
def get_vbox_instance(self, vbox_id):
"""
Returns a VirtualBox VM instance.
:param vbox_id: VirtualBox VM identifier
:returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
log.debug("VirtualBox VM ID {} doesn't exist".format(vbox_id), exc_info=1)
self.send_custom_error("VirtualBox VM ID {} doesn't exist".format(vbox_id))
return None
return self._vbox_instances[vbox_id]
@IModule.route("virtualbox.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all VirtualBox instances
for vbox_id in self._vbox_instances:
vbox_instance = self._vbox_instances[vbox_id]
vbox_instance.delete()
# resets the instance IDs
VirtualBoxVM.reset()
self._vbox_instances.clear()
self._allocated_udp_ports.clear()
if self._vboxwrapper and self._vboxwrapper.connected():
self._vboxwrapper.send("vboxwrapper reset")
log.info("VirtualBox module has been reset")
@IModule.route("virtualbox.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- working_dir (path to a working directory)
- vboxwrapper_path (path to vboxwrapper)
- 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 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))
if "vboxwrapper_path" in request:
self._vboxwrapper_path = request["vboxwrapper_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"]
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("virtualbox.create")
def vbox_create(self, request):
"""
Creates a new VirtualBox VM instance.
Mandatory request parameters:
- name (VirtualBox VM name)
- vmname (VirtualBox VM name in VirtualBox)
Optional request parameters:
- console (VirtualBox VM console port)
Response parameters:
- id (VirtualBox VM instance identifier)
- name (VirtualBox VM name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_CREATE_SCHEMA):
return
name = request["name"]
vmname = request["vmname"]
console = request.get("console")
vbox_id = request.get("vbox_id")
try:
if not self._vboxwrapper and not self._vboxmanager:
self._start_vbox_service()
vbox_instance = VirtualBoxVM(self._vboxwrapper,
self._vboxmanager,
name,
vmname,
self._working_dir,
self._host,
vbox_id,
console,
self._console_start_port_range,
self._console_end_port_range)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"name": vbox_instance.name,
"id": vbox_instance.id}
defaults = vbox_instance.defaults()
response.update(defaults)
self._vbox_instances[vbox_instance.id] = vbox_instance
self.send_response(response)
@IModule.route("virtualbox.delete")
def vbox_delete(self, request):
"""
Deletes a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_DELETE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.clean_delete()
del self._vbox_instances[request["id"]]
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.update")
def vbox_update(self, request):
"""
Updates a VirtualBox VM instance
Mandatory request parameters:
- id (VirtualBox 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, VBOX_UPDATE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
# update the VirtualBox VM settings
response = {}
for name, value in request.items():
if hasattr(vbox_instance, name) and getattr(vbox_instance, name) != value:
try:
setattr(vbox_instance, name, value)
response[name] = value
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("virtualbox.start")
def vbox_start(self, request):
"""
Starts a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_START_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.start()
except VirtualBoxError as e:
if self._vboxwrapper:
self.send_custom_error("{}: {}".format(e, self._vboxwrapper.read_stderr()))
else:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.stop")
def vbox_stop(self, request):
"""
Stops a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.stop()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.reload")
def vbox_reload(self, request):
"""
Reloads a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_RELOAD_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.reload()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.stop")
def vbox_stop(self, request):
"""
Stops a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.stop()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.suspend")
def vbox_suspend(self, request):
"""
Suspends a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_SUSPEND_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
try:
vbox_instance.suspend()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (VirtualBox 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, VBOX_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_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(vbox_instance.name,
vbox_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("virtualbox.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a VirtualBox VM instance.
Mandatory request parameters:
- id (VirtualBox 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, VBOX_ADD_NIO_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_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 VirtualBoxError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
if not nio:
raise VirtualBoxError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
try:
vbox_instance.port_add_nio_binding(port, nio)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("virtualbox.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
try:
nio = vbox_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 VirtualBoxError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("virtualbox.start_capture")
def vbox_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_START_CAPTURE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
try:
capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name)
vbox_instance.start_capture(port, capture_file_path)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("virtualbox.stop_capture")
def vbox_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VBOX_STOP_CAPTURE_SCHEMA):
return
# get the instance
vbox_instance = self.get_vbox_instance(request["id"])
if not vbox_instance:
return
port = request["port"]
try:
vbox_instance.stop_capture(port)
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("virtualbox.vm_list")
def vm_list(self, request):
"""
Gets VirtualBox VM list.
Response parameters:
- Server address/host
- List of VM names
"""
if not self._vboxwrapper and not self._vboxmanager:
try:
self._start_vbox_service()
except VirtualBoxError as e:
self.send_custom_error(str(e))
return
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!")
return
response = {"server": self._host,
"vms": vms}
self.send_response(response)
@IModule.route("virtualbox.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)
Whitespace-only changes.
@@ -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
@@ -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):
"""
VirtualBox Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "VirtualBox Ethernet adapter"
Whitespace-only changes.
+65
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
@@ -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"
+476
View File
@@ -0,0 +1,476 @@
# -*- 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()
+428
View File
@@ -0,0 +1,428 @@
# -*- 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/>.
VBOX_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new VirtualBox VM instance",
"type": "object",
"properties": {
"name": {
"description": "VirtualBox VM instance name",
"type": "string",
"minLength": 1,
},
"vmname": {
"description": "VirtualBox VM name (in VirtualBox itself)",
"type": "string",
"minLength": 1,
},
"vbox_id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
},
"additionalProperties": False,
"required": ["name", "vmname"],
}
VBOX_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"name": {
"description": "VirtualBox VM instance name",
"type": "string",
"minLength": 1,
},
"vmname": {
"description": "VirtualBox VM name (in VirtualBox itself)",
"type": "string",
"minLength": 1,
},
"adapters": {
"description": "number of adapters",
"type": "integer",
"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",
"minLength": 1,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"enable_console": {
"description": "enable the console",
"type": "boolean"
},
"headless": {
"description": "headless mode",
"type": "boolean"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VBOX_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
VBOX_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a VirtualBox 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": "VirtualBox VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"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"]
}
VBOX_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a VirtualBox VM instance",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
},
"additionalProperties": False,
"required": ["id", "port"]
}
VBOX_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a VirtualBox VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id", "capture_file_name"]
}
VBOX_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a VirtualBox VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VirtualBox VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 36 # maximum given by the ICH9 chipset in VirtualBox
},
"port_id": {
"description": "Unique port identifier for the VirtualBox VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port", "port_id"]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,612 @@
"""
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)
@@ -0,0 +1,422 @@
# -*- 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 pkg_resources import parse_version
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._stderr_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:
with open(os.devnull, "w") as null:
self._stderr_file = fd.name
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
self._process = subprocess.Popen(self._command,
stdout=null,
stderr=fd,
cwd=self._working_dir)
log.info("VirtualBox wrapper started PID={}".format(self._process.pid))
time.sleep(0.1) # give some time for vboxwrapper to start
if self._process.poll() is not None:
raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(self.read_stderr()))
self.wait_for_vboxwrapper(self._host, self._port)
self.connect()
self._started = True
version = self.send('vboxwrapper version')[0]
if parse_version(version) < parse_version("0.9.1"):
self.stop()
raise VirtualBoxError("VirtualBox wrapper version must be >= 0.9.1")
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._stderr_file and os.access(self._stderr_file, os.W_OK):
try:
os.remove(self._stderr_file)
except OSError as e:
log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e))
self._started = False
def read_stderr(self):
"""
Reads the standard error output of the VirtualBox wrapper process.
Only use when the process has been stopped or has crashed.
"""
output = ""
if self._stderr_file and os.access(self._stderr_file, os.R_OK):
try:
with open(self._stderr_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._stderr_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
@@ -0,0 +1,557 @@
# -*- 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/>.
"""
Controls VirtualBox using the VBox API.
"""
import sys
import os
import tempfile
import re
import time
import socket
import subprocess
if sys.platform.startswith('win'):
import msvcrt
import win32file
from .virtualbox_error import VirtualBoxError
from .pipe_proxy import PipeProxy
import logging
log = logging.getLogger(__name__)
class VirtualBoxController(object):
def __init__(self, vmname, vboxmanager, host):
self._host = host
self._machine = None
self._session = None
self._vboxmanager = vboxmanager
self._maximum_adapters = 0
self._serial_pipe_thread = None
self._serial_pipe = None
self._vmname = vmname
self._console = 0
self._adapters = []
self._headless = False
self._enable_console = True
self._adapter_type = "Automatic"
try:
self._machine = self._vboxmanager.vbox.findMachine(self._vmname)
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
# The maximum support network cards depends on the Chipset (PIIX3 or ICH9)
self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType)
@property
def vmname(self):
return self._vmname
@vmname.setter
def vmname(self, new_vmname):
self._vmname = new_vmname
try:
self._machine = self._vboxmanager.vbox.findMachine(new_vmname)
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
# The maximum support network cards depends on the Chipset (PIIX3 or ICH9)
self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType)
@property
def console(self):
return self._console
@console.setter
def console(self, console):
self._console = console
@property
def headless(self):
return self._headless
@headless.setter
def headless(self, headless):
self._headless = headless
@property
def enable_console(self):
return self._enable_console
@enable_console.setter
def enable_console(self, enable_console):
self._enable_console = enable_console
@property
def adapters(self):
return self._adapters
@adapters.setter
def adapters(self, adapters):
self._adapters = adapters
@property
def adapter_type(self):
return self._adapter_type
@adapter_type.setter
def adapter_type(self, adapter_type):
self._adapter_type = adapter_type
def start(self):
if len(self._adapters) > self._maximum_adapters:
raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters))
if self._machine.state == self._vboxmanager.constants.MachineState_Paused:
self.resume()
return
self._get_session()
self._set_network_options()
if self._enable_console:
self._set_console_options()
progress = self._launch_vm_process()
log.info("VM is starting with {}% completed".format(progress.percent))
if progress.percent != 100:
# This will happen if you attempt to start VirtualBox with unloaded "vboxdrv" module.
# or have too little RAM or damaged vHDD, or connected to non-existent network.
# We must unlock machine, otherwise it locks the VirtualBox Manager GUI. (on Linux hosts)
self._unlock_machine()
raise VirtualBoxError("Unable to start the VM (failed at {}%)".format(progress.percent))
try:
self._machine.setGuestPropertyValue("NameInGNS3", self._name)
except Exception:
pass
if self._enable_console:
# starts the Telnet to pipe thread
pipe_name = self._get_pipe_name()
if sys.platform.startswith('win'):
try:
self._serial_pipe = open(pipe_name, "a+b")
except OSError as e:
raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e))
self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console)
#self._serial_pipe_thread.setDaemon(True)
self._serial_pipe_thread.start()
else:
try:
self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._serial_pipe.connect(pipe_name)
except OSError as e:
raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e))
self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console)
#self._serial_pipe_thread.setDaemon(True)
self._serial_pipe_thread.start()
def stop(self):
if self._serial_pipe_thread:
self._serial_pipe_thread.stop()
self._serial_pipe_thread.join(1)
if self._serial_pipe_thread.isAlive():
log.warn("Serial pire thread is still alive!")
self._serial_pipe_thread = None
if self._serial_pipe:
if sys.platform.startswith('win'):
win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno()))
else:
self._serial_pipe.close()
self._serial_pipe = None
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
try:
if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ:
# work around VirtualBox bug #9239
vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname)
subprocess.call(command, timeout=3)
else:
progress = self._session.console.powerDown()
# wait for VM to actually go down
progress.waitForCompletion(3000)
log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent))
self._lock_machine()
for adapter_id in range(0, len(self._adapters)):
if self._adapters[adapter_id] is None:
continue
self._disable_adapter(adapter_id, disable=True)
if self._enable_console:
serial_port = self._session.machine.getSerialPort(0)
serial_port.enabled = False
self._session.machine.saveSettings()
self._unlock_machine()
except Exception as e:
# Do not crash "vboxwrapper", if stopping VM fails.
# But return True anyway, so VM state in GNS3 can become "stopped"
# This can happen, if user manually kills VBox VM.
log.warn("could not stop VM for {}: {}".format(self._vmname, e))
return
def suspend(self):
try:
self._session.console.pause()
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
def reload(self):
try:
self._session.console.reset()
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
def resume(self):
try:
self._session.console.resume()
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
def _get_session(self):
log.debug("getting session for {}".format(self._vmname))
try:
self._session = self._vboxmanager.mgr.getSessionObject(self._vboxmanager.vbox)
except Exception as e:
# fails on heavily loaded hosts...
raise VirtualBoxError("VirtualBox error: {}".format(e))
def _set_network_options(self):
log.debug("setting network options for {}".format(self._vmname))
self._lock_machine()
first_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM
try:
first_adapter = self._session.machine.getNetworkAdapter(0)
first_adapter_type = first_adapter.adapterType
except Exception as e:
pass
#raise VirtualBoxError("VirtualBox error: {}".format(e))
for adapter_id in range(0, len(self._adapters)):
try:
# VirtualBox starts counting from 0
adapter = self._session.machine.getNetworkAdapter(adapter_id)
if self._adapters[adapter_id] is None:
# force enable to avoid any discrepancy in the interface numbering inside the VM
# e.g. Ethernet2 in GNS3 becoming eth0 inside the VM when using a start index of 2.
adapter.enabled = True
continue
vbox_adapter_type = adapter.adapterType
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A
if self._adapter_type == "PCNet-FAST III (Am79C973)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973
if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM
if self._adapter_type == "Intel PRO/1000 T Server (82543GC)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC
if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM
if self._adapter_type == "Paravirtualized Network (virtio-net)":
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio
if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC"
vbox_adapter_type = first_adapter_type
adapter.adapterType = vbox_adapter_type
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
nio = self._adapters[adapter_id].get_nio(0)
if nio:
log.debug("setting UDP params on adapter {}".format(adapter_id))
try:
adapter.enabled = True
adapter.cableConnected = True
adapter.traceEnabled = False
# Temporary hack around VBox-UDP patch limitation: inability to use DNS
if nio.rhost == 'localhost':
rhost = '127.0.0.1'
else:
rhost = nio.rhost
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic
adapter.genericDriver = "UDPTunnel"
adapter.setProperty("sport", str(nio.lport))
adapter.setProperty("dest", rhost)
adapter.setProperty("dport", str(nio.rport))
except Exception as e:
# usually due to COM Error: "The object is not ready"
raise VirtualBoxError("VirtualBox error: {}".format(e))
if nio.capturing:
self._enable_capture(adapter, nio.pcap_output_file)
else:
# shutting down unused adapters...
try:
adapter.enabled = True
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
adapter.cableConnected = False
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
for adapter_id in range(len(self._adapters), self._maximum_adapters):
log.debug("disabling remaining adapter {}".format(adapter_id))
self._disable_adapter(adapter_id)
try:
self._session.machine.saveSettings()
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
self._unlock_machine()
def _disable_adapter(self, adapter_id, disable=True):
log.debug("disabling network adapter for {}".format(self._vmname))
# this command is retried several times, because it fails more often...
retries = 6
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not disable network adapter after 4 retries: {}".format(last_exception))
try:
adapter = self._session.machine.getNetworkAdapter(adapter_id)
adapter.traceEnabled = False
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
if disable:
adapter.enabled = False
break
except Exception as e:
# usually due to COM Error: "The object is not ready"
log.warn("cannot disable network adapter for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
last_exception = e
time.sleep(1)
continue
def _enable_capture(self, adapter, output_file):
log.debug("enabling capture for {}".format(self._vmname))
# this command is retried several times, because it fails more often...
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not enable packet capture after 4 retries: {}".format(last_exception))
try:
adapter.traceEnabled = True
adapter.traceFile = output_file
break
except Exception as e:
log.warn("cannot enable packet capture for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
last_exception = e
time.sleep(0.75)
continue
def create_udp(self, adapter_id, sport, daddr, dport):
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
# the machine is being executed
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not create an UDP tunnel after 4 retries :{}".format(last_exception))
try:
adapter = self._session.machine.getNetworkAdapter(adapter_id)
adapter.cableConnected = True
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
self._session.machine.saveSettings()
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic
adapter.genericDriver = "UDPTunnel"
adapter.setProperty("sport", str(sport))
adapter.setProperty("dest", daddr)
adapter.setProperty("dport", str(dport))
self._session.machine.saveSettings()
break
except Exception as e:
# usually due to COM Error: "The object is not ready"
log.warn("cannot create UDP tunnel for {}: {}".format(self._vmname, e))
last_exception = e
time.sleep(0.75)
continue
def delete_udp(self, adapter_id):
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
# the machine is being executed
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not delete an UDP tunnel after 4 retries :{}".format(last_exception))
try:
adapter = self._session.machine.getNetworkAdapter(adapter_id)
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
adapter.cableConnected = False
self._session.machine.saveSettings()
break
except Exception as e:
# usually due to COM Error: "The object is not ready"
log.debug("cannot delete UDP tunnel for {}: {}".format(self._vmname, e))
last_exception = e
time.sleep(0.75)
continue
def _get_pipe_name(self):
p = re.compile('\s+', re.UNICODE)
pipe_name = p.sub("_", self._vmname)
if sys.platform.startswith('win'):
pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name)
else:
pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name))
return pipe_name
def _set_console_options(self):
"""
# Example to manually set serial parameters using Python
from vboxapi import VirtualBoxManager
mgr = VirtualBoxManager(None, None)
mach = mgr.vbox.findMachine("My VM")
session = mgr.mgr.getSessionObject(mgr.vbox)
mach.lockMachine(session, 1)
mach2=session.machine
serial_port = mach2.getSerialPort(0)
serial_port.enabled = True
serial_port.path = "/tmp/test_pipe"
serial_port.hostMode = 1
serial_port.server = True
session.unlockMachine()
"""
log.info("setting console options for {}".format(self._vmname))
self._lock_machine()
pipe_name = self._get_pipe_name()
try:
serial_port = self._session.machine.getSerialPort(0)
serial_port.enabled = True
serial_port.path = pipe_name
serial_port.hostMode = 1
serial_port.server = True
self._session.machine.saveSettings()
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
self._unlock_machine()
def _launch_vm_process(self):
log.debug("launching VM {}".format(self._vmname))
# this command is retried several times, because it fails more often...
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not launch the VM after 4 retries: {}".format(last_exception))
try:
if self._headless:
mode = "headless"
else:
mode = "gui"
log.info("starting {} in {} mode".format(self._vmname, mode))
progress = self._machine.launchVMProcess(self._session, mode, "")
break
except Exception as e:
# This will usually happen if you try to start the same VM twice,
# but may happen on loaded hosts too...
log.warn("cannot launch VM {}, retrying {}: {}".format(self._vmname, retry + 1, e))
last_exception = e
time.sleep(0.6)
continue
try:
progress.waitForCompletion(-1)
except Exception as e:
raise VirtualBoxError("VirtualBox error: {}".format(e))
return progress
def _lock_machine(self):
log.debug("locking machine for {}".format(self._vmname))
# this command is retried several times, because it fails more often...
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not lock the machine after 4 retries: {}".format(last_exception))
try:
self._machine.lockMachine(self._session, 1)
break
except Exception as e:
log.warn("cannot lock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
last_exception = e
time.sleep(1)
continue
def _unlock_machine(self):
log.debug("unlocking machine for {}".format(self._vmname))
# this command is retried several times, because it fails more often...
retries = 4
last_exception = None
for retry in range(retries):
if retry == (retries - 1):
raise VirtualBoxError("Could not unlock the machine after 4 retries: {}".format(last_exception))
try:
self._session.unlockMachine()
break
except Exception as e:
log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
time.sleep(1)
last_exception = e
continue
@@ -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 VirtualBox module.
"""
class VirtualBoxError(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
@@ -0,0 +1,654 @@
# -*- 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/>.
"""
VirtualBox VM instance.
"""
import os
import shutil
from .virtualbox_error import VirtualBoxError
from .virtualbox_controller import VirtualBoxController
from .adapters.ethernet_adapter import EthernetAdapter
from ..attic import find_unused_port
import logging
log = logging.getLogger(__name__)
class VirtualBoxVM(object):
"""
VirtualBox VM implementation.
:param vboxwrapper client: VboxWrapperClient instance
:param vboxmanager: VirtualBox manager from the VirtualBox API
:param name: name of this VirtualBox VM
:param vmname: name of this VirtualBox VM in VirtualBox itself
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param vbox_id: VirtalBox VM instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
"""
_instances = []
_allocated_console_ports = []
def __init__(self,
vboxwrapper,
vboxmanager,
name,
vmname,
working_dir,
host="127.0.0.1",
vbox_id=None,
console=None,
console_start_port_range=4512,
console_end_port_range=5000):
if not vbox_id:
self._id = 0
for identifier in range(1, 1024):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise VirtualBoxError("Maximum number of VirtualBox VM instances reached")
else:
if vbox_id in self._instances:
raise VirtualBoxError("VirtualBox identifier {} is already used by another VirtualBox VM instance".format(vbox_id))
self._id = vbox_id
self._instances.append(self._id)
self._name = name
self._working_dir = None
self._host = host
self._command = []
self._vboxwrapper = vboxwrapper
self._started = False
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# VirtualBox settings
self._console = console
self._ethernet_adapters = []
self._headless = False
self._enable_console = True
self._vmname = vmname
self._adapter_start_index = 0
self._adapter_type = "Automatic"
working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id))
if vbox_id and not os.path.isdir(working_dir_path):
raise VirtualBoxError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise VirtualBoxError(e)
if self._console in self._allocated_console_ports:
raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console))
self._allocated_console_ports.append(self._console)
if self._vboxwrapper:
self._vboxwrapper.send('vbox create vbox "{}"'.format(self._name))
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console))
else:
self._vboxcontroller = VirtualBoxController(self._vmname, vboxmanager, self._host)
self._vboxcontroller.console = self._console
self.adapters = 2 # creates 2 adapters by default
log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name,
id=self._id))
def defaults(self):
"""
Returns all the default attribute values for this VirtualBox VM.
:returns: default values (dictionary)
"""
vbox_defaults = {"name": self._name,
"vmname": self._vmname,
"adapters": self.adapters,
"adapter_start_index": self._adapter_start_index,
"adapter_type": "Automatic",
"console": self._console,
"enable_console": self._enable_console,
"headless": self._headless}
return vbox_defaults
@property
def id(self):
"""
Returns the unique ID for this VirtualBox VM.
:returns: id (integer)
"""
return self._id
@classmethod
def reset(cls):
"""
Resets allocated instance list.
"""
cls._instances.clear()
cls._allocated_console_ports.clear()
@property
def name(self):
"""
Returns the name of this VirtualBox VM.
:returns: name
"""
return self._name
@name.setter
def name(self, new_name):
"""
Sets the name of this VirtualBox VM.
:param new_name: name
"""
log.info("VirtualBox VM {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
if self._vboxwrapper:
self._vboxwrapper.send('vbox rename "{}" "{}"'.format(self._name, new_name))
self._name = new_name
@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 this VirtualBox VM.
:param working_dir: path to the working directory
"""
try:
os.makedirs(working_dir)
except FileExistsError:
pass
except OSError as e:
raise VirtualBoxError("Could not create working directory {}: {}".format(working_dir, e))
self._working_dir = working_dir
log.info("VirtualBox VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
@property
def console(self):
"""
Returns the TCP console port.
:returns: console port (integer)
"""
return self._console
@console.setter
def console(self, console):
"""
Sets the TCP console port.
:param console: console port (integer)
"""
if console in self._allocated_console_ports:
raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console))
else:
self._vboxcontroller.console = console
log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
def delete(self):
"""
Deletes this VirtualBox VM.
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console and self.console in self._allocated_console_ports:
self._allocated_console_ports.remove(self.console)
if self._vboxwrapper:
self._vboxwrapper.send('vbox delete "{}"'.format(self._name))
log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
def clean_delete(self):
"""
Deletes this VirtualBox VM & all files.
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
if self._vboxwrapper:
self._vboxwrapper.send('vbox delete "{}"'.format(self._name))
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete VirtualBox VM {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("VirtualBox VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property
def headless(self):
"""
Returns either the VM will start in headless mode
:returns: boolean
"""
return self._headless
@headless.setter
def headless(self, headless):
"""
Sets either the VM will start in headless mode
:param headless: boolean
"""
if headless:
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name))
else:
self._vboxcontroller.headless = True
log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id))
else:
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" headless_mode False'.format(self._name))
else:
self._vboxcontroller.headless = False
log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id))
self._headless = headless
@property
def enable_console(self):
"""
Returns either the console is enabled or not
:returns: boolean
"""
return self._enable_console
@enable_console.setter
def enable_console(self, enable_console):
"""
Sets either the console is enabled or not
:param enable_console: boolean
"""
if enable_console:
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" enable_console True'.format(self._name))
else:
self._vboxcontroller.enable_console = True
log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id))
else:
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" enable_console False'.format(self._name))
else:
self._vboxcontroller.enable_console = False
log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id))
self._enable_console = enable_console
@property
def vmname(self):
"""
Returns the VM name associated with this VirtualBox VM.
:returns: VirtualBox VM name
"""
return self._vmname
@vmname.setter
def vmname(self, vmname):
"""
Sets the VM name associated with this VirtualBox VM.
:param vmname: VirtualBox VM name
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
else:
self._vboxcontroller.vmname = vmname
log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname))
self._vmname = vmname
@property
def adapters(self):
"""
Returns the number of Ethernet adapters for this VirtualBox VM instance.
:returns: number of adapters
"""
return len(self._ethernet_adapters)
@adapters.setter
def adapters(self, adapters):
"""
Sets the number of Ethernet adapters for this VirtualBox VM instance.
:param adapters: number of adapters
"""
self._ethernet_adapters.clear()
for adapter_id in range(0, self._adapter_start_index + adapters):
if adapter_id < self._adapter_start_index:
self._ethernet_adapters.append(None)
continue
self._ethernet_adapters.append(EthernetAdapter())
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, adapters))
else:
self._vboxcontroller.adapters = self._ethernet_adapters
log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
id=self._id,
adapters=adapters))
@property
def adapter_start_index(self):
"""
Returns the adapter start index for this VirtualBox VM instance.
:returns: index
"""
return self._adapter_start_index
@adapter_start_index.setter
def adapter_start_index(self, adapter_start_index):
"""
Sets the adapter start index for this VirtualBox VM instance.
:param adapter_start_index: index
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" nic_start_index {}'.format(self._name, adapter_start_index))
self._adapter_start_index = adapter_start_index
self.adapters = self.adapters # this forces to recreate the adapter list with the correct index
log.info("VirtualBox VM {name} [id={id}]: adapter start index changed to {index}".format(name=self._name,
id=self._id,
index=adapter_start_index))
@property
def adapter_type(self):
"""
Returns the adapter type for this VirtualBox VM instance.
:returns: adapter type (string)
"""
return self._adapter_type
@adapter_type.setter
def adapter_type(self, adapter_type):
"""
Sets the adapter type for this VirtualBox VM instance.
:param adapter_type: adapter type (string)
"""
self._adapter_type = adapter_type
if self._vboxwrapper:
self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type))
else:
self._vboxcontroller.adapter_type = adapter_type
log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
id=self._id,
adapter_type=adapter_type))
def start(self):
"""
Starts this VirtualBox VM.
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox start "{}"'.format(self._name))
else:
self._vboxcontroller.start()
def stop(self):
"""
Stops this VirtualBox VM.
"""
if self._vboxwrapper:
try:
self._vboxwrapper.send('vbox stop "{}"'.format(self._name))
except VirtualBoxError:
# probably lost the connection
return
else:
self._vboxcontroller.stop()
def suspend(self):
"""
Suspends this VirtualBox VM.
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox suspend "{}"'.format(self._name))
else:
self._vboxcontroller.suspend()
def reload(self):
"""
Reloads this VirtualBox VM.
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox reset "{}"'.format(self._name))
else:
self._vboxcontroller.reload()
def resume(self):
"""
Resumes this VirtualBox VM.
"""
if self._vboxwrapper:
self._vboxwrapper.send('vbox resume "{}"'.format(self._name))
else:
self._vboxcontroller.resume()
def port_add_nio_binding(self, adapter_id, nio):
"""
Adds a port NIO binding.
:param adapter_id: adapter ID
:param nio: NIO instance to add to the slot/port
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name,
adapter_id=adapter_id))
if self._vboxwrapper:
self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name,
adapter_id,
nio.lport,
nio.rhost,
nio.rport))
else:
self._vboxcontroller.create_udp(adapter_id, nio.lport, nio.rhost, nio.rport)
adapter.add_nio(0, nio)
log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
id=self._id,
nio=nio,
adapter_id=adapter_id))
def port_remove_nio_binding(self, adapter_id):
"""
Removes a port NIO binding.
:param adapter_id: adapter ID
:returns: NIO instance
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name,
adapter_id=adapter_id))
if self._vboxwrapper:
self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name,
adapter_id))
else:
self._vboxcontroller.delete_udp(adapter_id)
nio = adapter.get_nio(0)
adapter.remove_nio(0)
log.info("VirtualBox VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name,
id=self._id,
nio=nio,
adapter_id=adapter_id))
return nio
def start_capture(self, adapter_id, output_file):
"""
Starts a packet capture.
:param adapter_id: adapter ID
:param output_file: PCAP destination file for the capture
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name,
adapter_id=adapter_id))
nio = adapter.get_nio(0)
if nio.capturing:
raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise VirtualBoxError("Could not create captures directory {}".format(e))
nio.startPacketCapture(output_file)
if self._vboxwrapper:
self._vboxwrapper.send('vbox create_capture "{}" {} "{}"'.format(self._name,
adapter_id,
output_file))
log.info("VirtualBox VM {name} [id={id}]: starting packet capture on adapter {adapter_id}".format(name=self._name,
id=self._id,
adapter_id=adapter_id))
def stop_capture(self, adapter_id):
"""
Stops a packet capture.
:param adapter_id: adapter ID
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name,
adapter_id=adapter_id))
nio = adapter.get_nio(0)
nio.stopPacketCapture()
if self._vboxwrapper:
self._vboxwrapper.send('vbox delete_capture "{}" {}'.format(self._name,
adapter_id))
log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name,
id=self._id,
adapter_id=adapter_id))
+66 -37
View File
@@ -20,16 +20,12 @@ VPCS server module.
"""
import os
import sys
import base64
import tempfile
import struct
import socket
import shutil
from gns3server.modules import IModule
from gns3server.config import Config
import gns3server.jsonrpc as jsonrpc
from .vpcs_device import VPCSDevice
from .vpcs_error import VPCSError
from .nios.nio_udp import NIO_UDP
@@ -45,6 +41,7 @@ from .schemas import VPCS_RELOAD_SCHEMA
from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import VPCS_ADD_NIO_SCHEMA
from .schemas import VPCS_DELETE_NIO_SCHEMA
from .schemas import VPCS_EXPORT_CONFIG_SCHEMA
import logging
log = logging.getLogger(__name__)
@@ -64,9 +61,9 @@ class VPCS(IModule):
# get the VPCS location
config = Config.instance()
vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs")
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:
@@ -84,12 +81,12 @@ class VPCS(IModule):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {}
self._console_start_port_range = 4512
self._console_end_port_range = 5000
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 = 40001
self._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._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
@@ -101,7 +98,6 @@ class VPCS(IModule):
:param signum: signal number (if called by the signal handler)
"""
# self._vpcs_callback.stop()
# delete all VPCS instances
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
@@ -162,7 +158,7 @@ class VPCS(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
return
@@ -194,7 +190,7 @@ class VPCS(IModule):
self._working_dir = new_working_dir
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.working_dir = self._working_dir
vpcs_instance.working_dir = os.path.join(self._working_dir, "vpcs", "pc-{}".format(vpcs_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"]
@@ -211,8 +207,10 @@ class VPCS(IModule):
"""
Creates a new VPCS instance.
Optional request parameters:
Mandatory request parameters:
- name (VPCS name)
Optional request parameters:
- console (VPCS console port)
Response parameters:
@@ -224,29 +222,23 @@ class VPCS(IModule):
"""
# validate the request
if request and not self.validate_request(request, VPCS_CREATE_SCHEMA):
if not self.validate_request(request, VPCS_CREATE_SCHEMA):
return
name = console = None
if request:
name = request.get("name")
console = request.get("console")
name = request["name"]
console = request.get("console")
vpcs_id = request.get("vpcs_id")
try:
try:
os.makedirs(self._working_dir)
except FileExistsError:
pass
except OSError as e:
raise VPCSError("Could not create working directory {}".format(e))
if not self._vpcs:
raise VPCSError("No path to a VPCS executable has been set")
vpcs_instance = VPCSDevice(self._vpcs,
vpcs_instance = VPCSDevice(name,
self._vpcs,
self._working_dir,
self._host,
name,
vpcs_id,
console,
self._console_start_port_range,
self._console_end_port_range)
@@ -326,7 +318,7 @@ class VPCS(IModule):
try:
if "script_file_base64" in request:
# a new startup-config has been pushed
config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
try:
@@ -341,7 +333,7 @@ class VPCS(IModule):
if os.path.isfile(request["script_file"]) and request["script_file"] != config_path:
# this is a local file set in the GUI
try:
with open(request["script_file"], "r") as f:
with open(request["script_file"], "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = config.replace("\r", "")
@@ -370,7 +362,7 @@ class VPCS(IModule):
self.send_response(response)
@IModule.route("vpcs.start")
def vm_start(self, request):
def vpcs_start(self, request):
"""
Starts a VPCS instance.
@@ -400,7 +392,7 @@ class VPCS(IModule):
self.send_response(True)
@IModule.route("vpcs.stop")
def vm_stop(self, request):
def vpcs_stop(self, request):
"""
Stops a VPCS instance.
@@ -430,7 +422,7 @@ class VPCS(IModule):
self.send_response(True)
@IModule.route("vpcs.reload")
def vm_reload(self, request):
def vpcs_reload(self, request):
"""
Reloads a VPCS instance.
@@ -502,8 +494,8 @@ class VPCS(IModule):
port,
self._host))
response = {"lport": port}
response["port_id"] = request["port_id"]
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("vpcs.add_nio")
@@ -554,7 +546,7 @@ class VPCS(IModule):
nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"]
if not self.has_privileged_access(self._vpcs, tap_device):
if not self.has_privileged_access(self._vpcs):
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, tap_device))
nio = NIO_TAP(tap_device)
if not nio:
@@ -606,6 +598,43 @@ class VPCS(IModule):
self.send_response(True)
@IModule.route("vpcs.export_config")
def export_config(self, request):
"""
Exports the script file from a VPCS instance.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- script_file_base64 (script file base64 encoded)
- False if no configuration can be exported
"""
# validate the request
if not self.validate_request(request, VPCS_EXPORT_CONFIG_SCHEMA):
return
# get the instance
vpcs_instance = self.get_vpcs_instance(request["id"])
if not vpcs_instance:
return
response = {}
script_file_path = os.path.join(vpcs_instance.working_dir, vpcs_instance.script_file)
try:
with open(script_file_path, "rb") as f:
config = f.read()
response["script_file_base64"] = base64.encodebytes(config).decode("utf-8")
except OSError as e:
self.send_custom_error("unable to export the script file: {}".format(e))
return
if not response:
self.send_response(False)
else:
self.send_response(response)
@IModule.route("vpcs.echo")
def echo(self, request):
"""
@@ -614,7 +643,7 @@ class VPCS(IModule):
:param request: JSON request
"""
if request == None:
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
+1 -1
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)
"""
+1 -1
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
+140 -121
View File
@@ -26,6 +26,10 @@ VPCS_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"vpcs_id": {
"description": "VPCS device instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
@@ -34,6 +38,7 @@ VPCS_CREATE_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["name"]
}
VPCS_DELETE_SCHEMA = {
@@ -150,129 +155,129 @@ VPCS_ADD_NIO_SCHEMA = {
"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
"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": {
@@ -326,3 +331,17 @@ VPCS_DELETE_NIO_SCHEMA = {
"additionalProperties": False,
"required": ["id", "port"]
}
VPCS_EXPORT_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to export the script file of a VPCS instance",
"type": "object",
"properties": {
"id": {
"description": "VPCS device instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
+75 -43
View File
@@ -25,7 +25,9 @@ import sys
import subprocess
import signal
import shutil
import re
from pkg_resources import parse_version
from .vpcs_error import VPCSError
from .adapters.ethernet_adapter import EthernetAdapter
from .nios.nio_udp import NIO_UDP
@@ -40,10 +42,11 @@ class VPCSDevice(object):
"""
VPCS device implementation.
: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 name: name of this VPCS device
:param vpcs_id: VPCS instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
@@ -53,39 +56,43 @@ class VPCSDevice(object):
_allocated_console_ports = []
def __init__(self,
name,
path,
working_dir,
host="127.0.0.1",
name=None,
vpcs_id=None,
console=None,
console_start_port_range=4512,
console_end_port_range=5000):
# find an instance identifier (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible
# MAC addresses given in VPCS using the -m option
self._id = 0
for identifier in range(1, 256):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise VPCSError("Maximum number of VPCS instances reached")
if not vpcs_id:
# find an instance identifier is none is provided (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible
# MAC addresses given in VPCS using the -m option
self._id = 0
for identifier in range(1, 256):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if name:
self._name = name
if self._id == 0:
raise VPCSError("Maximum number of VPCS instances reached")
else:
self._name = "VPCS{}".format(self._id)
if vpcs_id in self._instances:
raise VPCSError("VPCS identifier {} is already used by another VPCS device".format(vpcs_id))
self._id = vpcs_id
self._instances.append(self._id)
self._name = name
self._path = path
self._console = console
self._working_dir = None
self._host = host
self._command = []
self._process = None
self._vpcs_stdout_file = ""
self._host = "127.0.0.1"
self._started = False
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
@@ -94,8 +101,13 @@ class VPCSDevice(object):
self._script_file = ""
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
# update the working directory
self.working_dir = working_dir
working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
if vpcs_id and not os.path.isdir(working_dir_path):
raise VPCSError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
@@ -135,7 +147,7 @@ class VPCSDevice(object):
:returns: id (integer)
"""
return(self._id)
return self._id
@classmethod
def reset(cls):
@@ -166,10 +178,10 @@ class VPCSDevice(object):
if self._script_file:
# update the startup.vpc
config_path = os.path.join(self.working_dir, "startup.vpc")
config_path = os.path.join(self._working_dir, "startup.vpc")
if os.path.isfile(config_path):
try:
with open(config_path, "r+") as f:
with open(config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self._name, new_name)
f.seek(0)
@@ -178,8 +190,8 @@ class VPCSDevice(object):
raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e))
log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
id=self._id,
new_name=new_name))
self._name = new_name
@property
@@ -190,7 +202,7 @@ class VPCSDevice(object):
:returns: path to VPCS
"""
return(self._path)
return self._path
@path.setter
def path(self, path):
@@ -202,8 +214,8 @@ class VPCSDevice(object):
self._path = path
log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name,
id=self._id,
path=path))
id=self._id,
path=path))
@property
def working_dir(self):
@@ -223,8 +235,6 @@ class VPCSDevice(object):
:param working_dir: path to the working directory
"""
# create our own working directory
working_dir = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
try:
os.makedirs(working_dir)
except FileExistsError:
@@ -234,8 +244,8 @@ class VPCSDevice(object):
self._working_dir = working_dir
log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
id=self._id,
wd=self._working_dir))
@property
def console(self):
@@ -262,8 +272,8 @@ class VPCSDevice(object):
self._console = console
self._allocated_console_ports.append(self._console)
log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
id=self._id,
port=console))
def command(self):
"""
@@ -280,13 +290,14 @@ class VPCSDevice(object):
"""
self.stop()
self._instances.remove(self._id)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
if self.console and self.console in self._allocated_console_ports:
self._allocated_console_ports.remove(self.console)
log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
id=self._id))
def clean_delete(self):
"""
@@ -294,7 +305,8 @@ class VPCSDevice(object):
"""
self.stop()
self._instances.remove(self._id)
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
@@ -320,6 +332,23 @@ class VPCSDevice(object):
return self._started
def _check_vpcs_version(self):
"""
Checks if the VPCS executable version is >= 0.5b1.
"""
try:
output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
if parse_version(version) < parse_version("0.5b1"):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else:
raise VPCSError("Could not determine the VPCS version for {}".format(self._path))
except (OSError, subprocess.CalledProcessError) as e:
raise VPCSError("Error while looking for the VPCS version: {}".format(e))
def start(self):
"""
Starts the VPCS process.
@@ -331,10 +360,12 @@ class VPCSDevice(object):
raise VPCSError("No path to a VPCS executable has been set")
if not os.path.isfile(self._path):
raise VPCSError("VPCS '{}' is not accessible".format(self._path))
raise VPCSError("VPCS program '{}' is not accessible".format(self._path))
if not os.access(self._path, os.X_OK):
raise VPCSError("VPCS '{}' is not executable".format(self._path))
raise VPCSError("VPCS program '{}' is not executable".format(self._path))
self._check_vpcs_version()
if not self._ethernet_adapter.get_nio(0):
raise VPCSError("This VPCS instance must be connected in order to start")
@@ -344,6 +375,7 @@ class VPCSDevice(object):
log.info("starting VPCS: {}".format(self._command))
self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log")
log.info("logging to {}".format(self._vpcs_stdout_file))
flags = 0
if sys.platform.startswith("win32"):
flags = subprocess.CREATE_NEW_PROCESS_GROUP
with open(self._vpcs_stdout_file, "w") as fd:
@@ -386,7 +418,7 @@ class VPCSDevice(object):
output = ""
if self._vpcs_stdout_file:
try:
with open(self._vpcs_stdout_file) as file:
with open(self._vpcs_stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e))
@@ -399,7 +431,7 @@ class VPCSDevice(object):
:returns: True or False
"""
if self._process and self._process.poll() == None:
if self._process and self._process.poll() is None:
return True
return False
@@ -413,7 +445,7 @@ class VPCSDevice(object):
if not self._ethernet_adapter.port_exists(port_id):
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_id=port_id))
port_id=port_id))
self._ethernet_adapter.add_nio(port_id, nio)
log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name,
@@ -516,7 +548,7 @@ class VPCSDevice(object):
"""
Sets the script-file for this VPCS instance.
:param base_script_file: path to base-script-file
:param script_file: path to base-script-file
"""
self._script_file = script_file
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more